sunstone 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 5c574fe947ca4f3c808d4b1d843ae0e2bb4ce9ea
4
+ data.tar.gz: a2410c511c64d0f2b7a85dbc5cbbff3b23a2f7a1
5
+ SHA512:
6
+ metadata.gz: 3837b844459bdf78f20fd3562f44b6a91cb53c3fa3f4474d32bf3e7375b96400a2b39a45e907ff0fc2cf0d60730d2984606f01a13bfa7b03a01e330878d5be75
7
+ data.tar.gz: bf4c9c42165f389bc6e5e1475b19641a24d41ed6b7f8007fa37a549fdf6917656db7ebdcb609156b649d3c793eac053db84aa9fad2be3fabcc603430e8009d61
@@ -0,0 +1,29 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+
12
+ ## Documentation cache and generated files:
13
+ /.yardoc/
14
+ /_yardoc/
15
+ /doc/
16
+ /rdoc/
17
+
18
+ ## Environment normalisation:
19
+ /.bundle/
20
+ /lib/bundler/man/
21
+
22
+ # for a library or gem, you might want to ignore these files since the code is
23
+ # intended to run in multiple environments; otherwise, check them in:
24
+ Gemfile.lock
25
+ .ruby-version
26
+ .ruby-gemset
27
+
28
+ # Text Editor scraps
29
+ *~
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in sunstone.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Jon Bracy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ sunstone
2
+ ========
3
+
4
+ A library for interacting with REST APIs
@@ -0,0 +1,37 @@
1
+ require 'bundler/setup'
2
+ require "bundler/gem_tasks"
3
+ Bundler.require(:development)
4
+ require 'rake/testtask'
5
+ require 'rdoc/task'
6
+
7
+ task :console do
8
+ exec 'irb -I lib -r sunstone.rb'
9
+ end
10
+ task :c => :console
11
+
12
+ Rake::TestTask.new do |t|
13
+ t.libs << 'lib' << 'test'
14
+ t.test_files = FileList['test/**/*_test.rb']
15
+ #t.warning = true
16
+ #t.verbose = true
17
+ end
18
+
19
+ Rake::RDocTask.new do |rd|
20
+ rd.main = 'README.md'
21
+ rd.title = 'Sunstone Documentation'
22
+ rd.rdoc_dir = 'doc'
23
+
24
+ rd.options << '-f' << 'sdoc'
25
+ rd.options << '-T' << '42floors'
26
+ rd.options << '-g' # Generate github links
27
+
28
+ rd.rdoc_files.include('README.rdoc')
29
+ rd.rdoc_files.include('lib/**/*.rb')
30
+ end
31
+
32
+ desc "Run tests"
33
+ task :default => :test
34
+
35
+ namespace :pages do
36
+ #TODO: https://github.com/defunkt/sdoc-helpers/blob/master/lib/sdoc_helpers/pages.rb
37
+ end
data/TODO.md ADDED
@@ -0,0 +1,89 @@
1
+ - Check if `Sunstone#to_key` needs to be added
2
+
3
+ - Add `Sunstone::Model::Persistance` with the following methods:
4
+
5
+ - `#new_record?`
6
+ - `#persisted?`
7
+ - `#save`
8
+ - `#save!`
9
+ - `#update`
10
+ - `#update!`
11
+ - `#create`
12
+ - `#to_param` ?
13
+ - `::all`
14
+ - `::where` (probably goes in an Arel like engine)
15
+ - `::build`
16
+ - `::create!`
17
+ - `#==`
18
+ - `::create`
19
+
20
+ ```ruby
21
+ # Creates an object and saves it to the MLS. The resulting object is returned
22
+ # whether or no the object was saved successfully to the MLS or not.
23
+ #
24
+ # ==== Examples
25
+ # #!ruby
26
+ # # Create a single new object
27
+ # User.create(:first_name => 'Jamie')
28
+ #
29
+ # # Create a single object and pass it into a block to set other attributes.
30
+ # User.create(:first_name => 'Jamie') do |u|
31
+ # u.is_admin = false
32
+ # end
33
+ def self.create(attributes={}, &block) # TODO: testme
34
+ model = self.new(attributes)
35
+ yield(model) if block_given?
36
+ model.save
37
+ model
38
+ end
39
+
40
+
41
+ - Look at https://gist.github.com/malomalo/91f360fe52db3dbe1c99 files for inspiration, came from
42
+ Rails code I think
43
+
44
+ - Simplify `Sunstone::Type::Value` to `Sunstone::Type`
45
+
46
+ - Add a `find_class(type)` in `Sunstone::Schema`
47
+
48
+ - Possibly use Classes to hold information about each attribute in addition to the type
49
+
50
+ ```ruby
51
+ class MLS::Attribute
52
+
53
+ DEFAULT_OPTIONS = { :serialize => true }
54
+
55
+ attr_reader :model, :name, :instance_variable_name, :options, :default
56
+ attr_reader :reader_visibility, :writer_visibility
57
+
58
+ def initialize(name, options={})
59
+ @name = name
60
+ @instance_variable_name = "@#{@name}".freeze
61
+ @options = DEFAULT_OPTIONS.merge(options)
62
+
63
+ @default = @options[:default]
64
+ @reader_visibility = @options[:reader] || :public
65
+ @writer_visibility = @options[:writer] || :public
66
+ end
67
+ end
68
+ ```
69
+
70
+ - Use Association classes to model the association:
71
+
72
+ ```ruby
73
+ class MLS::Association
74
+ class BelongsTo
75
+ attr_reader :klass, :foreign_key, :foreign_type, :primary_key, :polymorphic
76
+
77
+ def initialize(name, options={})
78
+ @name = name
79
+ @klass = options[:class_name] ? options[:class_name].constantize : name.camelize.constantize
80
+
81
+ @polymorphic = options[:polymorphic] || false
82
+ @foreign_key = options[:foreign_key] || "#{name}_id".to_sym
83
+ @foreign_type = options[:foreign_type] || "#{name}_type".to_sym
84
+ @primary_key = options[:primary_key] || :id
85
+ end
86
+ end
87
+
88
+ end
89
+ ```
@@ -0,0 +1,361 @@
1
+ require 'set'
2
+ require 'uri'
3
+ require 'net/https'
4
+
5
+ require 'wankel'
6
+ require 'cookie_store'
7
+ require 'connection_pool'
8
+
9
+ require 'active_support'
10
+ require 'active_support/core_ext'
11
+
12
+ require 'active_model'
13
+
14
+ require 'sunstone/exception'
15
+ require 'sunstone/schema'
16
+ require 'sunstone/model'
17
+ require 'sunstone/parser'
18
+
19
+ # _Sunstone_ is a low-level API. It provides basic HTTP #get, #post, #put, and
20
+ # #delete calls to the Sunstone Server. It can also provides basic error
21
+ # checking of responses.
22
+ module Sunstone
23
+ VERSION = 0.1
24
+
25
+ extend self
26
+
27
+ attr_reader :site
28
+
29
+ # Set the User-Agent of the client. Will be joined with other User-Agent info
30
+ attr_writer :user_agent
31
+ attr_accessor :api_key, :host, :port, :use_ssl
32
+
33
+ # Sets the Protocol, API Token, and Host and Port of the API Server
34
+ #
35
+ # #!ruby
36
+ # Sunstone.site = "https://API_KEY@host.com"
37
+ def site=(url)
38
+ @site = url
39
+ uri = URI.parse(url)
40
+ @api_key = uri.user ? CGI.unescape(uri.user) : nil
41
+ @host, @port = uri.host, uri.port
42
+ @use_ssl = (uri.scheme == 'https')
43
+ end
44
+
45
+ # Returns the User-Agent of the client. Defaults to:
46
+ # "sunstone-ruby/SUNSTONE_VERSION RUBY_VERSION-pPATCH_LEVEL PLATFORM"
47
+ def user_agent
48
+ [
49
+ @user_agent,
50
+ "Sunstone/#{Sunstone::VERSION}",
51
+ "Ruby/#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}",
52
+ RUBY_PLATFORM
53
+ ].compact.join(' ')
54
+ end
55
+
56
+ # Set a cookie jar to use during request sent during the
57
+ def with_cookie_store(store, &block)
58
+ Thread.current[:sunstone_cookie_store] = store
59
+ yield
60
+ ensure
61
+ Thread.current[:sunstone_cookie_store] = nil
62
+ end
63
+
64
+ # Sends a Net::HTTPRequest to the server. The headers returned from
65
+ # Sunestone#headers are automatically added to the request. The appropriate
66
+ # error is raised if the response is not in the 200..299 range.
67
+ #
68
+ # Paramaters::
69
+ #
70
+ # * +request+ - A Net::HTTPRequest to send to the server
71
+ # * +body+ - Optional, a String, IO Object, or a Ruby object which is
72
+ # converted into JSON and sent as the body
73
+ # * +block+ - An optional block to call with the +Net::HTTPResponse+ object.
74
+ #
75
+ # Return Value::
76
+ #
77
+ # Returns the return value of the <tt>&block</tt> if given, otherwise the
78
+ # response object (a Net::HTTPResponse)
79
+ #
80
+ # Examples:
81
+ #
82
+ # #!ruby
83
+ # Sunstone.send_request(#<Net::HTTP::Get>) # => #<Net::HTTP::Response>
84
+ #
85
+ # Sunstone.send_request(#<Net::HTTP::Get @path="/404">) # => raises Sunstone::Exception::NotFound
86
+ #
87
+ # # this will still raise an exception if the response_code is not valid
88
+ # # and the block will not be called
89
+ # Sunstone.send_request(#<Net::HTTP::Get>) do |response|
90
+ # # ...
91
+ # end
92
+ #
93
+ # # The following example shows how to stream a response:
94
+ # Sunstone.send_request(#<Net::HTTP::Get>) do |response|
95
+ # response.read_body do |chunk|
96
+ # io.write(chunk)
97
+ # end
98
+ # end
99
+ def send_request(request, body=nil, &block)
100
+ request_headers.each { |k, v| request[k] = v }
101
+
102
+ if body.is_a?(IO)
103
+ request['Transfer-Encoding'] = 'chunked'
104
+ request.body_stream = body
105
+ elsif body.is_a?(String)
106
+ request.body = body
107
+ elsif body
108
+ request.body = Wankel.encode(body)
109
+ end
110
+
111
+ return_value = nil
112
+ with_connection do |connection|
113
+ connection.request(request) do |response|
114
+
115
+ if response['X-42Floors-API-Version-Deprecated']
116
+ logger.warn("DEPRECATION WARNING: API v#{API_VERSION} is being phased out")
117
+ end
118
+
119
+ validate_response_code(response)
120
+
121
+ # Get the cookies
122
+ response.each_header do |key, value|
123
+ if key.downcase == 'set-cookie' && Thread.current[:sunstone_cookie_store]
124
+ Thread.current[:sunstone_cookie_store].set_cookie("#{site}#{request.path}", value)
125
+ end
126
+ end
127
+
128
+ if block_given?
129
+ return_value =yield(response)
130
+ else
131
+ return_value =response
132
+ end
133
+ end
134
+ end
135
+
136
+ return_value
137
+ end
138
+
139
+ # Send a GET request to +path+ on the Sunstone Server via +Sunstone#send_request+.
140
+ # See +Sunstone#send_request+ for more details on how the response is handled.
141
+ #
142
+ # Paramaters::
143
+ #
144
+ # * +path+ - The +path+ on the server to GET to.
145
+ # * +params+ - Either a String, Hash, or Ruby Object that responds to
146
+ # #to_param. Appended on the URL as query params
147
+ # * +block+ - An optional block to call with the +Net::HTTPResponse+ object.
148
+ #
149
+ # Return Value::
150
+ #
151
+ # See +Sunstone#send_request+
152
+ #
153
+ # Examples:
154
+ #
155
+ # #!ruby
156
+ # Sunstone.get('/example') # => #<Net::HTTP::Response>
157
+ #
158
+ # Sunstone.get('/example', 'query=stuff') # => #<Net::HTTP::Response>
159
+ #
160
+ # Sunstone.get('/example', {:query => 'stuff'}) # => #<Net::HTTP::Response>
161
+ #
162
+ # Sunstone.get('/404') # => raises Sunstone::Exception::NotFound
163
+ #
164
+ # Sunstone.get('/act') do |response|
165
+ # # ...
166
+ # end
167
+ def get(path, params='', &block)
168
+ params ||= ''
169
+ request = Net::HTTP::Get.new(path + '?' + params.to_param)
170
+
171
+ send_request(request, nil, &block)
172
+ end
173
+
174
+ # Send a POST request to +path+ on the Sunstone Server via +Sunstone#send_request+.
175
+ # See +Sunstone#send_request+ for more details on how the response is handled.
176
+ #
177
+ # Paramaters::
178
+ #
179
+ # * +path+ - The +path+ on the server to POST to.
180
+ # * +body+ - Optional, See +Sunstone#send_request+.
181
+ # * +block+ - Optional, See +Sunstone#send_request+
182
+ #
183
+ # Return Value::
184
+ #
185
+ # See +Sunstone#send_request+
186
+ #
187
+ # Examples:
188
+ #
189
+ # #!ruby
190
+ # Sunstone.post('/example') # => #<Net::HTTP::Response>
191
+ #
192
+ # Sunstone.post('/example', 'body') # => #<Net::HTTP::Response>
193
+ #
194
+ # Sunstone.post('/example', #<IO Object>) # => #<Net::HTTP::Response>
195
+ #
196
+ # Sunstone.post('/example', {:example => 'data'}) # => #<Net::HTTP::Response>
197
+ #
198
+ # Sunstone.post('/404') # => raises Sunstone::Exception::NotFound
199
+ #
200
+ # Sunstone.post('/act') do |response|
201
+ # # ...
202
+ # end
203
+ def post(path, body=nil, &block)
204
+ request = Net::HTTP::Post.new(path)
205
+
206
+ send_request(request, body, &block)
207
+ end
208
+
209
+ # Send a PUT request to +path+ on the Sunstone Server via +Sunstone#send_request+.
210
+ # See +Sunstone#send_request+ for more details on how the response is handled.
211
+ #
212
+ # Paramaters::
213
+ #
214
+ # * +path+ - The +path+ on the server to POST to.
215
+ # * +body+ - Optional, See +Sunstone#send_request+.
216
+ # * +block+ - Optional, See +Sunstone#send_request+
217
+ #
218
+ # Return Value::
219
+ #
220
+ # See +Sunstone#send_request+
221
+ #
222
+ # Examples:
223
+ #
224
+ # #!ruby
225
+ # Sunstone.put('/example') # => #<Net::HTTP::Response>
226
+ #
227
+ # Sunstone.put('/example', 'body') # => #<Net::HTTP::Response>
228
+ #
229
+ # Sunstone.put('/example', #<IO Object>) # => #<Net::HTTP::Response>
230
+ #
231
+ # Sunstone.put('/example', {:example => 'data'}) # => #<Net::HTTP::Response>
232
+ #
233
+ # Sunstone.put('/404') # => raises Sunstone::Exception::NotFound
234
+ #
235
+ # Sunstone.put('/act') do |response|
236
+ # # ...
237
+ # end
238
+ def put(path, body=nil, *valid_response_codes, &block)
239
+ request = Net::HTTP::Put.new(path)
240
+
241
+ send_request(request, body, &block)
242
+ end
243
+
244
+ # Send a DELETE request to +path+ on the Sunstone Server via +Sunstone#send_request+.
245
+ # See +Sunstone#send_request+ for more details on how the response is handled
246
+ #
247
+ # Paramaters::
248
+ #
249
+ # * +path+ - The +path+ on the server to POST to.
250
+ # * +block+ - Optional, See +Sunstone#send_request+
251
+ #
252
+ # Return Value::
253
+ #
254
+ # See +Sunstone#send_request+
255
+ #
256
+ # Examples:
257
+ #
258
+ # #!ruby
259
+ # Sunstone.delete('/example') # => #<Net::HTTP::Response>
260
+ #
261
+ # Sunstone.delete('/404') # => raises Sunstone::Exception::NotFound
262
+ #
263
+ # Sunstone.delete('/act') do |response|
264
+ # # ...
265
+ # end
266
+ def delete(path, &block)
267
+ request = Net::HTTP::Delete.new(path)
268
+
269
+ send_request(request, nil, &block)
270
+ end
271
+
272
+ # Get a connection from the connection pool and perform the block with
273
+ # the connection
274
+ def with_connection(&block)
275
+ connection_pool.with({}, &block)
276
+ end
277
+
278
+ # Ping the Sunstone. If everything is configured and operating correctly
279
+ # <tt>"pong"</tt> will be returned. Otherwise and Sunstone::Exception should be
280
+ # thrown.
281
+ #
282
+ # #!ruby
283
+ # Sunstone.ping # => "pong"
284
+ #
285
+ # Sunstone.ping # raises Sunstone::Exception::ServiceUnavailable if a 503 is returned
286
+ def ping
287
+ get('/ping').body
288
+ end
289
+
290
+ def config
291
+ @config ||= Wankel.parse(get('/config').body, :symbolize_keys => true)
292
+ end
293
+
294
+ private
295
+
296
+ def request_headers
297
+ headers = {
298
+ 'Content-Type' => 'application/json',
299
+ 'User-Agent' => user_agent
300
+ }
301
+
302
+ headers['Api-Key'] = api_key if api_key
303
+
304
+ headers
305
+ end
306
+
307
+ # Raise an Sunstone::Exception based on the response_code, unless the response_code
308
+ # is include in the valid_response_codes Array
309
+ #
310
+ # Paramaters::
311
+ #
312
+ # * +response+ - The Net::HTTP::Response object
313
+ #
314
+ # Return Value::
315
+ #
316
+ # If an exception is not raised the +response+ is returned
317
+ #
318
+ # Examples:
319
+ #
320
+ # #!ruby
321
+ # Sunstone.validate_response_code(<Net::HTTP::Response @code=200>) # => <Net::HTTP::Response @code=200>
322
+ #
323
+ # Sunstone.validate_response_code(<Net::HTTP::Response @code=404>) # => raises Sunstone::Exception::NotFound
324
+ #
325
+ # Sunstone.validate_response_code(<Net::HTTP::Response @code=500>) # => raises Sunstone::Exception
326
+ def validate_response_code(response)
327
+ code = response.code.to_i
328
+
329
+ if !(200..299).include?(code)
330
+ case code
331
+ when 400
332
+ raise Sunstone::Exception::BadRequest, response
333
+ when 401
334
+ raise Sunstone::Exception::Unauthorized, response
335
+ when 404
336
+ raise Sunstone::Exception::NotFound, response
337
+ when 410
338
+ raise Sunstone::Exception::Gone, response
339
+ when 422
340
+ raise Sunstone::Exception::ApiVersionUnsupported, response
341
+ when 503
342
+ raise Sunstone::Exception::ServiceUnavailable, response
343
+ when 301
344
+ raise Sunstone::Exception::MovedPermanently, response
345
+ when 300..599
346
+ raise Sunstone::Exception, response
347
+ else
348
+ raise Sunstone::Exception, response
349
+ end
350
+ end
351
+ end
352
+
353
+ def connection_pool
354
+ @connection_pool ||= ConnectionPool.new(size: 5, timeout: 5) do
355
+ connection = Net::HTTP.new(@host, @port)
356
+ connection.use_ssl = @ssl
357
+ connection
358
+ end
359
+ end
360
+
361
+ end