spore 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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