sunstone 0.1.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,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