spore 0.0.3

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.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 hallelujah
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,31 @@
1
+ = SPORE
2
+
3
+ Spore is a specification for describing ReST API that can be parsed and used
4
+ automatically by client implementations to communicate with the descibed API
5
+
6
+ For the SPORE spec please refer to
7
+ http://github.com/SPORE
8
+
9
+ A blog entry by Franck Cuny about SPORE, explaining why it makes sense:
10
+ http://lumberjaph.net/misc/2010/10/20/spore-update.html
11
+
12
+ This project is a Ruby implementation of SPORE.
13
+
14
+ == Note on Patches/Pull Requests
15
+
16
+ * Fork the project.
17
+ * Make your feature addition or bug fix.
18
+ * Add tests for it. This is important so I don't break it in a
19
+ future version unintentionally.
20
+ * Commit, do not mess with rakefile, version, or history.
21
+ (if you want to have your own version, that is fine but bump version in a
22
+ commit by itself I can ignore when I pull)
23
+ * Send me a pull request. Bonus points for topic branches.
24
+
25
+ == Copyright
26
+
27
+ Copyright (c) 2010 Weborama. See LICENSE for details.
28
+
29
+ == Contributors
30
+ - Alexis Sukrieh <sukria@sukria.net>
31
+ - Hery Ramihajamalala <hery@rails-royce.org>
@@ -0,0 +1,58 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+ gem 'rdoc', '>= 2.5.11'
4
+ require 'rake'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = "spore"
10
+ gem.summary = %Q{A Ruby implementation for SPORE}
11
+ gem.description = %Q{Spore is a specification for describing ReST API that can be parsed and used automatically by client implementations to communicate with the descibed API}
12
+ gem.email = "<sukria@sukria.net>"
13
+ gem.homepage = "http://github.com/sukria/Ruby-Spore"
14
+ gem.authors = ["Alexis Sukrieh <sukria@sukria.net> [sukria]", "Hery Ramihajamalala <hery@rails-royce.org> [hallelujah]"]
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ gem.add_dependency "json", ">= 1.4.6"
17
+ gem.add_dependency "httpclient"
18
+ end
19
+ Jeweler::GemcutterTasks.new
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
22
+ end
23
+
24
+ require 'rake/testtask'
25
+ Rake::TestTask.new(:test) do |test|
26
+ test.libs << 'lib' << 'test'
27
+ test.pattern = 'test/**/test_*.rb'
28
+ test.verbose = true
29
+ test.options = '--verbose'
30
+ end
31
+
32
+ begin
33
+ require 'rcov/rcovtask'
34
+ Rcov::RcovTask.new do |test|
35
+ test.libs << 'test'
36
+ test.pattern = 'test/**/test_*.rb'
37
+ test.verbose = true
38
+ test.rcov_opts += ["--exclude /gems/"]
39
+ end
40
+ rescue LoadError
41
+ task :rcov do
42
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
43
+ end
44
+ end
45
+
46
+ task :test => :check_dependencies
47
+
48
+ task :default => :test
49
+
50
+ require 'rake/rdoctask'
51
+ Rake::RDocTask.new do |rdoc|
52
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
53
+
54
+ rdoc.rdoc_dir = 'rdoc'
55
+ rdoc.title = "spore #{version}"
56
+ rdoc.rdoc_files.include('README*')
57
+ rdoc.rdoc_files.include('lib/**/*.rb')
58
+ end
@@ -0,0 +1,5 @@
1
+ == TODO List
2
+
3
+ * Make more modular
4
+ * define_method is too long
5
+ * Implement Spore specs
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.3
@@ -0,0 +1,290 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+ require 'uri'
4
+ require 'net/http'
5
+ require 'httpclient'
6
+
7
+ # we need to be able to build an HTTPResponse object,
8
+ # and apparently, there's no way to do that outside of the HTTPResponse class
9
+ # WTF?
10
+ module HTTP
11
+ class Message
12
+ def body=(value)
13
+ @body = value
14
+ end
15
+ end
16
+ end
17
+
18
+ # SPORE
19
+ class Spore
20
+
21
+ attr_accessor :name, :author
22
+ attr_accessor :base_url, :format, :version
23
+ attr_accessor :methods
24
+ attr_accessor :middlewares
25
+ attr_reader :specs
26
+
27
+ class RequiredParameterExptected < Exception
28
+ end
29
+
30
+ class UnexpectedResponse < Exception
31
+ end
32
+
33
+ class UnsupportedSpec < Exception
34
+ end
35
+
36
+ class InvalidHeaders < Exception
37
+ end
38
+
39
+ ##
40
+ # Initialize a Spore instance with a specification file<br/>
41
+ # Optionally a file to require the parser from and the custom bound Parser class
42
+ #
43
+ # :call-seq:
44
+ # new(file_path, options = {} )
45
+ #
46
+ # Spore.new('/tmp/github.json')
47
+ #
48
+ # or
49
+ #
50
+ # Spore.new('/tmp/spec.dot', :require => 'my_custom_lib', :parser => 'DotParser')
51
+ #
52
+ # DotParser must implement a class method load_file
53
+ #
54
+ # class DotParser
55
+ # def self.load_file(f)
56
+ # str = ""
57
+ # File.open(f) do |f|
58
+ # str = ...
59
+ # # Do what you have to here
60
+ # end
61
+ # end
62
+ # end
63
+ #
64
+ def initialize(spec,options = {})
65
+ # Don't load gems that are not needed
66
+ # Only when it requires json, then json is loaded
67
+ parser = self.class.load_parser(spec, options)
68
+ specs = parser.load_file(spec)
69
+
70
+ inititliaze_api_attrs(specs)
71
+ construct_client_class(self.methods)
72
+ self.middlewares = []
73
+ end
74
+
75
+ ##
76
+ # :call-seq:
77
+ # spore.enable(Spore::Middleware::SomeThing, :optionX => 42)
78
+ #
79
+ # This method takes a middleware class as its first argument.
80
+ # Options to pass to the middleware can be given as a second
81
+ # argument.
82
+ # Once this method is called, the Spore client will pass the request
83
+ # and the response objects to the middleware whenever appropriate.
84
+ # The order in which middlewares are enabled is respected by Spore.
85
+ # (same order for processing the request before it's sent, reverse
86
+ # order for processing response, as described in the Spore
87
+ # specification).
88
+ #
89
+
90
+ def enable(middleware, args={})
91
+ m = middleware.new(args)
92
+ self.middlewares.push({
93
+ :condition => Proc.new { true },
94
+ :middleware => m
95
+ })
96
+ end
97
+
98
+ ##
99
+ # :call-seq:
100
+ # spore.enable_if(Spore::Middleware::SomeThing, :optionX => 42) do |env| {
101
+ # # return true or false, depending on env to say if the
102
+ # # middleware should be enabled or not for this request
103
+ # }
104
+ #
105
+ # Same as the enable method, but is enabled dynamically, depending
106
+ # of the current request env.
107
+ # A block is given and is passed the env stack, it should return a
108
+ # boolean value to tell if the middleware should be enabled or not.
109
+ #
110
+ def enable_if(middleware, args={}, &block)
111
+ m = middleware.new(args)
112
+ self.middlewares.push({
113
+ :condition => block,
114
+ :middleware => m
115
+ })
116
+ end
117
+
118
+ ##
119
+ # :call-seq:
120
+ # load_parser(spec_file, options = {})
121
+ #
122
+ # This method takes two arguments spec_file and options<br/>
123
+ # If spec is a yml or json file options is skipped<br/>
124
+ # Else options is used for requiring and loading the correct parser<br/><br/>
125
+ # options is a Hash with :require and :parser keys.
126
+ # * :require is a file to require
127
+ # * :parser is a String to pass in Object.const_get
128
+ #
129
+
130
+ def self.load_parser(spec, options = {})
131
+ case spec
132
+ when /\.ya?ml/
133
+ require('spore/spec_parser/yaml')
134
+ Spore::SpecParser::Yaml
135
+ when /\.json/
136
+ require 'spore/spec_parser/json'
137
+ Spore::SpecParser::Json
138
+ else
139
+ if options.has_key?(:require)
140
+ require options[:require]
141
+ if options.has_key?(:parser)
142
+ Object.const_get(options[:parser])
143
+ else
144
+ Object.const_get(options[:require].to_s.capitalize)
145
+ end
146
+ else
147
+ raise UnsupportedSpec, "don't know how to parse '#{spec}'"
148
+ end
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def inititliaze_api_attrs(spec)
155
+ self.name = spec['name']
156
+ self.author = spec['author']
157
+ self.base_url = spec['base_url'].gsub(/\/$/, '')
158
+ self.format = spec['format']
159
+ self.version = spec['version']
160
+ self.methods = spec['methods']
161
+ end
162
+
163
+ # FIXME : collapse methods
164
+ # Hmmm I think it is not good
165
+ # If we use Github API and Facebook API in the same project, methods may be overriden
166
+
167
+ def construct_client_class(methods)
168
+ methods.keys.each do |m|
169
+ define_method(m, methods[m])
170
+ end
171
+ end
172
+
173
+ def define_method(name, m)
174
+ method = m['method'].downcase
175
+ path = m['path']
176
+ params = m['params']
177
+ required = m['required_params'] || m['required']
178
+ expected = m['expected']
179
+ desc = m['description']
180
+
181
+ mod = Module.new
182
+ mod.send(:define_method, name) do |args|
183
+
184
+ # make sure all mandatory params are sent
185
+ required.each do |mandatory|
186
+ if not args.has_key?(mandatory.to_sym)
187
+ raise RequiredParameterExptected, "parameter `#{mandatory}' expected"
188
+ end
189
+ end
190
+
191
+ # build the real path (expand named tokens)
192
+ real_path = path
193
+ while m = real_path.match(/:([^:\/\.]+)/)
194
+ if not args.has_key?(m[1].to_sym)
195
+ raise RequiredParameterExptected, "named token `#{m[1]}' expected"
196
+ end
197
+ real_path = real_path.gsub(/:#{m[1]}/, args[m[1].to_sym].to_s)
198
+ args.delete(m[1].to_sym)
199
+ end
200
+ full_path = "#{self.base_url}#{real_path}"
201
+
202
+ # build the ENV hash
203
+ env = {}
204
+ env['spore.request_method'] = method
205
+ env['spore.request_path'] = full_path
206
+ env['spore.request_params'] = args
207
+ env['spore.request_headers'] = []
208
+
209
+ response = nil
210
+
211
+ # call all middlewares
212
+ self.middlewares.each do |m|
213
+ if m[:condition].call(env)
214
+ response = m[:middleware].process_request(env)
215
+ break if response
216
+ end
217
+ end
218
+
219
+ # transoform the SPORE response to a valid HTTPResponse object
220
+ if response
221
+ response = to_http(response)
222
+ end
223
+
224
+ if not response
225
+
226
+ res = send_http_request(
227
+ env['spore.request_method'],
228
+ env['spore.request_path'],
229
+ env['spore.request_params'],
230
+ env['spore.request_headers'])
231
+
232
+ # parse the response and make sure we have expected result
233
+ if expected && (not expected.include?(res.code.to_i))
234
+ raise UnexpectedResponse, "response status: '#{res.code}' expected is: #{expected.to_json}"
235
+ end
236
+
237
+ response = res
238
+ end
239
+
240
+ # process response with middlewares in reverse orders
241
+ self.middlewares.reverse.each do |m|
242
+ if m[:condition].call(env)
243
+ response = m[:middleware].process_response(response, env)
244
+ end
245
+ end
246
+
247
+ response
248
+ end
249
+ self.extend(mod)
250
+ end
251
+
252
+ def send_http_request(method_name, path, params, headers)
253
+
254
+ # our HttpClient object
255
+ client = HTTPClient.new
256
+
257
+ # normalize our headers for HttpClient
258
+ h = headers.map{|header| header.values_at(:name,:value)}
259
+
260
+ # the response object we expect to have
261
+ resp = client.send(method_name,path, params, h) if method_name =~ %r{get|post|put|delete}
262
+
263
+ return resp
264
+ end
265
+
266
+ def to_http(spore_resp)
267
+ return nil if spore_resp.nil?
268
+ return spore_resp if spore_resp.class != Array
269
+
270
+ code = spore_resp[0]
271
+ headers = spore_resp[1]
272
+ body = spore_resp[2][0]
273
+
274
+ if headers.size % 2 != 0
275
+ raise InvalidHeaders, "Odd number of elements in SPORE headers"
276
+ end
277
+
278
+ r = Net::HTTPResponse.new('1.1', code, '')
279
+ i = 0
280
+ while i < headers.size
281
+ header = headers[i]
282
+ value = headers[i+1]
283
+ r.add_field(header, value)
284
+ i += 2
285
+ end
286
+
287
+ r.body = body if body
288
+ return r
289
+ end
290
+ end
@@ -0,0 +1,42 @@
1
+ # encoding: utf-8
2
+ class Spore
3
+ class Middleware
4
+
5
+ class ExpectedParam < Exception
6
+ end
7
+
8
+ # overide this list in your middleware
9
+ # if you need to store some atteributes and make sure
10
+ # they're initialized when the middleware is enabled
11
+ def expected_params
12
+ []
13
+ end
14
+
15
+ # you should not need to overrride this one
16
+ def initialize(args)
17
+ for param in self.expected_params
18
+ if not args.has_key?(param)
19
+ raise ExpectedParam, "param '#{param}' is expected"
20
+ end
21
+
22
+ self.class.send(:attr_accessor, param.to_sym)
23
+ eval "self.#{param} = args[param]"
24
+ end
25
+ end
26
+
27
+ # This is where your middleware can handle an incoming request _before_
28
+ # it's executed (the env hash contains anything to build the query)
29
+ # if you want to halt the process of the request, return a Net::HTTP
30
+ # response object
31
+ # if you just want to alter the env hash, do it and return nil
32
+ def process_request(env)
33
+ end
34
+
35
+ # This is where your middleware can alter the response object
36
+ # Make sure you return _always_ the response object
37
+ def process_response(response, env)
38
+ return response
39
+ end
40
+
41
+ end
42
+ end