ananke 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +15 -0
- data/README.rdoc +114 -0
- data/Rakefile +65 -0
- data/lib/ananke.rb +109 -0
- data/lib/version.rb +3 -0
- data/spec/cov_adapter.rb +9 -0
- data/spec/lib/ananke_spec.rb +126 -0
- data/spec/lib/validation_spec.rb +89 -0
- data/spec/nice_formatter.rb +346 -0
- data/spec/spec_helper.rb +17 -0
- metadata +154 -0
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gem "sinatra", "~>1.1.2"
|
4
|
+
|
5
|
+
group :development, :test do
|
6
|
+
gem "colored", "~>1.2"
|
7
|
+
gem "json", "~>1.5.1"
|
8
|
+
gem "rack-test", "~>0.5.6"
|
9
|
+
gem "rake", "~>0.8.7"
|
10
|
+
gem "rspec", "~>2.5.0"
|
11
|
+
gem 'simplecov', '~>0.3.9'
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
|
data/README.rdoc
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
= Ananke
|
2
|
+
|
3
|
+
Ananke is a DSL that extends the functionality of Sinatra for easy creation of Restful Services and Resources:
|
4
|
+
|
5
|
+
#myapp.rb
|
6
|
+
require 'ananke'
|
7
|
+
require 'sinatra/main' #This is only for Demo purposes
|
8
|
+
#--------------------Repositories---------------------
|
9
|
+
module Repository
|
10
|
+
module User
|
11
|
+
@data = [{:id => '1', :name => 'One'}, {:id => '2', :name => 'Two'}]
|
12
|
+
def self.all
|
13
|
+
@data.to_s
|
14
|
+
end
|
15
|
+
def self.one(id)
|
16
|
+
index = @data.index{ |d| d[:id] == id}
|
17
|
+
(index.nil? && '') || @data[index].to_s
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
#-------------------REST Resources--------------------
|
22
|
+
rest :user do
|
23
|
+
id :id
|
24
|
+
end
|
25
|
+
|
26
|
+
Install the gem and run with:
|
27
|
+
|
28
|
+
gem install ananke
|
29
|
+
ruby -rubygems myapp.rb
|
30
|
+
|
31
|
+
All Users
|
32
|
+
http://localhost:4567/user
|
33
|
+
One User
|
34
|
+
http://localhost:4567/user/1
|
35
|
+
|
36
|
+
== REST Resources
|
37
|
+
`rest` defines a complete Resource, and constructs Sinatra Routes
|
38
|
+
based on what's available in it's respective Repository. Routes are:
|
39
|
+
|
40
|
+
get '/name/?' -> Repository::Capitalize(name).all
|
41
|
+
get '/name/id' -> Repository::Capitalize(name).one(id)
|
42
|
+
post '/name' -> Repository::Capitalize(name).new(data)
|
43
|
+
put '/name/id' -> Repository::Capitalize(name).edit(id, data)
|
44
|
+
delete '/name/id' -> Repository::Capitalize(name).delete(id)
|
45
|
+
|
46
|
+
The REST media type can be built up:
|
47
|
+
|
48
|
+
required :name
|
49
|
+
optional :country
|
50
|
+
|
51
|
+
== Repositories
|
52
|
+
The Default Repository can be changed:
|
53
|
+
|
54
|
+
ananke.default_repository = 'MyRepository'
|
55
|
+
|
56
|
+
== Validation
|
57
|
+
Validation can be added on any field by providing arguments after a field declaration:
|
58
|
+
|
59
|
+
required :name, :length => 4
|
60
|
+
|
61
|
+
This will cause the paramater to be validated against the method defined in Ananke::Rules. Custom Rules
|
62
|
+
can be added to the module and provided as arguments. Current Default included rules are:
|
63
|
+
|
64
|
+
length(min)
|
65
|
+
|
66
|
+
Validation Methods are Invoked in the Ananke::Rules context, and has access to a class variable named
|
67
|
+
value, which holds the value for the currently valuated Parameter.
|
68
|
+
|
69
|
+
To Add a Custom Rule:
|
70
|
+
|
71
|
+
rule :name, do
|
72
|
+
value == [expected] ? nil : 'Not Expected Value'
|
73
|
+
end
|
74
|
+
|
75
|
+
required :name, :name
|
76
|
+
|
77
|
+
or
|
78
|
+
|
79
|
+
module Ananke
|
80
|
+
module Rules
|
81
|
+
def validate_name
|
82
|
+
value == [expected] ? nil : 'Not Expected Value'
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
required :name, :name
|
88
|
+
|
89
|
+
or Advanced
|
90
|
+
|
91
|
+
module Ananke
|
92
|
+
module Rules
|
93
|
+
def validate_name(extra)
|
94
|
+
value == [expected using extra] ? nil : 'Not Expected Value'
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
required :name, :name => extra
|
100
|
+
|
101
|
+
== Future
|
102
|
+
A short list of future development:
|
103
|
+
- Refactor!
|
104
|
+
- Return Value Strategy
|
105
|
+
- Resource Exposes Media Type
|
106
|
+
- HyperMedia
|
107
|
+
- Lots more `bullet-proofing`
|
108
|
+
- ETag Support
|
109
|
+
|
110
|
+
== More
|
111
|
+
|
112
|
+
* {Project Website}[http://https://github.com/HasAndries/ananke/]
|
113
|
+
* {Issue tracker}[https://github.com/HasAndries/ananke/issues]
|
114
|
+
* {Twitter}[http://twitter.com/HasAndries]
|
data/Rakefile
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require "rake"
|
3
|
+
require "rake/rdoctask"
|
4
|
+
require 'rake/gempackagetask'
|
5
|
+
require "rspec/core/rake_task"
|
6
|
+
|
7
|
+
require File.expand_path("../lib/version", __FILE__)
|
8
|
+
gemspec = Gem::Specification.new do |gem|
|
9
|
+
gem.name = "ananke"
|
10
|
+
gem.version = Ananke::VERSION
|
11
|
+
gem.platform = Gem::Platform::RUBY
|
12
|
+
gem.authors = ["Andries Coetzee"]
|
13
|
+
gem.email = "andriesc@mixtel.com"
|
14
|
+
gem.summary = "#{gem.name}-#{Ananke::VERSION}"
|
15
|
+
gem.description = "Full REST Implementation on top of Sinatra"
|
16
|
+
gem.homepage = "https://github.com/HasAndries/ananke"
|
17
|
+
|
18
|
+
gem.rubygems_version = "1.5.0"
|
19
|
+
|
20
|
+
gem.files = FileList['lib/**/*', 'spec/**/*', 'Gemfile', 'Rakefile', 'README.rdoc']
|
21
|
+
gem.test_files = FileList['spec/**/*']
|
22
|
+
gem.extra_rdoc_files = [ "README.rdoc" ]
|
23
|
+
gem.rdoc_options = ["--charset=UTF-8"]
|
24
|
+
gem.require_path = "lib"
|
25
|
+
|
26
|
+
gem.post_install_message = %Q{**************************************************
|
27
|
+
|
28
|
+
Thank you for installing #{gem.summary}
|
29
|
+
|
30
|
+
Please be sure to look at README.rdoc to see what might have changed
|
31
|
+
since the last release and how to use this GEM.
|
32
|
+
|
33
|
+
**************************************************
|
34
|
+
}
|
35
|
+
gem.add_dependency "sinatra", "~> 1.1.2"
|
36
|
+
|
37
|
+
gem.add_development_dependency "colored", "~> 1.2"
|
38
|
+
gem.add_development_dependency "json", "~> 1.5.1"
|
39
|
+
gem.add_development_dependency "rack-test", "~> 0.5.6"
|
40
|
+
gem.add_development_dependency "rake", "~> 0.8.7"
|
41
|
+
gem.add_development_dependency "rspec", "~> 2.5.0"
|
42
|
+
gem.add_development_dependency "simplecov", "~> 0.3.9"
|
43
|
+
end
|
44
|
+
|
45
|
+
Rake::GemPackageTask.new(gemspec) do |pkg|
|
46
|
+
pkg.need_tar = true
|
47
|
+
end
|
48
|
+
|
49
|
+
desc %{Build the gemspec file.}
|
50
|
+
task :gemspec do
|
51
|
+
gemspec.validate
|
52
|
+
File.open("#{gemspec.name}.gemspec", 'w'){|f| f.write gemspec.to_ruby }
|
53
|
+
end
|
54
|
+
|
55
|
+
RSpec::Core::RakeTask.new(:test) do |t|
|
56
|
+
t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
|
57
|
+
t.pattern = 'spec/**/*_spec.rb'
|
58
|
+
end
|
59
|
+
|
60
|
+
RSpec::Core::RakeTask.new(:doc) do |t|
|
61
|
+
t.rspec_opts = ["-c" "-r ./spec/nice_formatter.rb", "-f NiceFormatter", "-o doc.htm"]
|
62
|
+
t.pattern = 'spec/**/*_spec.rb'
|
63
|
+
end
|
64
|
+
|
65
|
+
task :default => [:test]
|
data/lib/ananke.rb
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
|
3
|
+
module Ananke
|
4
|
+
class << self
|
5
|
+
attr_accessor :default_repository, :rules
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
@default_repository = 'Repository'
|
10
|
+
@rules = [:length]
|
11
|
+
|
12
|
+
def build(path)
|
13
|
+
#TODO - Check if Modules Exist
|
14
|
+
mod = Module.const_get(Ananke.default_repository.to_sym).const_get("#{path.capitalize}".to_sym)
|
15
|
+
key = @id[:key]
|
16
|
+
fields = @fields
|
17
|
+
links = @links
|
18
|
+
|
19
|
+
#TODO - Check if Repository Supports Resource
|
20
|
+
Sinatra::Base.get "/#{path}/:#{key}" do
|
21
|
+
ret = mod.one(params[key]) if !params[key].nil?
|
22
|
+
#TODO - Hyper Links(Common place maybe?)
|
23
|
+
ret.respond_to?(:to_json) ? ret.to_json : ret
|
24
|
+
end
|
25
|
+
|
26
|
+
#TODO - Check if Repository Supports Resource
|
27
|
+
Sinatra::Base.get "/#{path}/?" do
|
28
|
+
ret = mod.all
|
29
|
+
#TODO - Hyper Links(Common place maybe?)
|
30
|
+
ret.respond_to?(:to_json) ? ret.to_json : ret
|
31
|
+
end
|
32
|
+
|
33
|
+
#TODO - Check if Repository Supports Resource
|
34
|
+
Sinatra::Base.post "/#{path}/?" do
|
35
|
+
#TODO - Parameter Validation
|
36
|
+
status, message = validate!(fields, params)
|
37
|
+
error status, message unless status.nil?
|
38
|
+
ret = mod.new(params)
|
39
|
+
status 201
|
40
|
+
#TODO - Hyper Links for Created Resource
|
41
|
+
end
|
42
|
+
|
43
|
+
#TODO - Check if Repository Supports Resource
|
44
|
+
Sinatra::Base.put "/#{path}/:#{key}" do
|
45
|
+
#TODO - Parameter Validation
|
46
|
+
mod.edit(params[key], params) if !params[key].nil?
|
47
|
+
status 200
|
48
|
+
#TODO - Hyper Links
|
49
|
+
end
|
50
|
+
|
51
|
+
#TODO - Check if Repository Supports Resource
|
52
|
+
Sinatra::Base.delete "/#{path}/:#{key}" do
|
53
|
+
#TODO - Parameter Validation
|
54
|
+
mod.delete(params[key]) if !params[key].nil?
|
55
|
+
status 200
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def validate!(fields, params)
|
60
|
+
errors = []
|
61
|
+
fields.each do |field|
|
62
|
+
value = params[field[:key].to_s]
|
63
|
+
errors << "Missing Required Parameter: #{field[:key]}" if field[:type] == :required && value.nil?
|
64
|
+
Ananke::Rules.value = value
|
65
|
+
field[:rules].each do |r|
|
66
|
+
res = r.class == Hash ? Ananke::Rules.send("validate_#{r.first[0]}", r.first[1]) : Ananke::Rules.send("validate_#{r}")
|
67
|
+
errors << "#{field[:key]}: #{res}" unless res.nil?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
return 400, errors unless errors.empty?
|
71
|
+
end
|
72
|
+
|
73
|
+
public
|
74
|
+
|
75
|
+
def rest(path, &block)
|
76
|
+
@id = {}
|
77
|
+
@fields = []
|
78
|
+
@links = []
|
79
|
+
yield block
|
80
|
+
build path
|
81
|
+
end
|
82
|
+
|
83
|
+
def id(key, *rules)
|
84
|
+
@id = {:key => key, :type => :id, :rules => rules}
|
85
|
+
end
|
86
|
+
def required(key, *rules)
|
87
|
+
@fields << {:key => key, :type => :required, :rules => rules}
|
88
|
+
end
|
89
|
+
def optional(key, *rules)
|
90
|
+
@fields << {:key => key, :type => :optional, :rules => rules}
|
91
|
+
end
|
92
|
+
def media(rel, method, resource, field)
|
93
|
+
@links << {:rel => rel, :method => method, :resource => resource, :field => field}
|
94
|
+
end
|
95
|
+
def rule(name, &block)
|
96
|
+
Ananke::Rules.send(:define_singleton_method, "validate_#{name}", block)
|
97
|
+
end
|
98
|
+
|
99
|
+
module Rules
|
100
|
+
class << self
|
101
|
+
attr_accessor :value
|
102
|
+
end
|
103
|
+
def self.validate_length(min)
|
104
|
+
value.length >= min ? nil : "Value must be at least #{min} characters long"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
include Ananke
|
data/lib/version.rb
ADDED
data/spec/cov_adapter.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require './spec/spec_helper'
|
2
|
+
require './lib/ananke'
|
3
|
+
|
4
|
+
describe 'Resource' do
|
5
|
+
include Rack::Test::Methods
|
6
|
+
include Ananke
|
7
|
+
|
8
|
+
def app
|
9
|
+
Sinatra::Base
|
10
|
+
end
|
11
|
+
|
12
|
+
before(:all) do
|
13
|
+
rest :user do
|
14
|
+
id :user_id
|
15
|
+
required :username
|
16
|
+
required 'email'
|
17
|
+
optional :country
|
18
|
+
|
19
|
+
media "Get All Vehicles", :get, :vehicles, :user_id
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it """
|
24
|
+
Should setup the defaults for ReST
|
25
|
+
""" do
|
26
|
+
Ananke.default_repository.should == 'Repository'
|
27
|
+
end
|
28
|
+
|
29
|
+
it """
|
30
|
+
Should setup Routes
|
31
|
+
""" do
|
32
|
+
Sinatra::Base.routes["GET"][-1][0].inspect.include?('user').should == true
|
33
|
+
Sinatra::Base.routes["GET"].length.should == 2
|
34
|
+
Sinatra::Base.routes["POST"][-1][0].inspect.include?('user').should == true
|
35
|
+
Sinatra::Base.routes["PUT"][-1][0].inspect.include?('user').should == true
|
36
|
+
Sinatra::Base.routes["DELETE"][-1][0].inspect.include?('user').should == true
|
37
|
+
end
|
38
|
+
|
39
|
+
#----------------------------BASIC--------------------------------------
|
40
|
+
it """
|
41
|
+
GET /user
|
42
|
+
- code: 200
|
43
|
+
- content-type: text/plain
|
44
|
+
- body: [{:id=>1, :name=>'one'}, {:id => 2, :name => 'two'}]
|
45
|
+
""" do
|
46
|
+
get "/user"
|
47
|
+
last_response.status.should == 200
|
48
|
+
last_response.body.should == Repository::User.data.to_json
|
49
|
+
end
|
50
|
+
|
51
|
+
it """
|
52
|
+
GET /user/1
|
53
|
+
- code: 200
|
54
|
+
- content-type: text/plain
|
55
|
+
- body: {user_id: ,username: ,email: ,country: }
|
56
|
+
""" do
|
57
|
+
get "/user/1"
|
58
|
+
last_response.status.should == 200
|
59
|
+
last_response.body.should == Repository::User.data[0].to_json
|
60
|
+
end
|
61
|
+
|
62
|
+
it """
|
63
|
+
POST /user
|
64
|
+
- body: {user_id: ,username: ,email: ,country: }
|
65
|
+
RETURN
|
66
|
+
- code: 201
|
67
|
+
- content-type: text/json
|
68
|
+
- body:
|
69
|
+
""" do
|
70
|
+
post "/user", body={:user_id => 3, :username => 'three', :email => '3@three.com', :country => 'USA'}
|
71
|
+
last_response.status.should == 201
|
72
|
+
last_response.body.should == ''
|
73
|
+
end
|
74
|
+
|
75
|
+
it """
|
76
|
+
PUT /user/3
|
77
|
+
- body: {user_id: ,username: ,email: ,country: }
|
78
|
+
RETURN
|
79
|
+
- code: 200
|
80
|
+
- content-type: text/json
|
81
|
+
- body:
|
82
|
+
""" do
|
83
|
+
put "/user/3", body={:user_id => 3, :username => 'four', :email => '4@four.com', :country => 'Russia'}
|
84
|
+
last_response.status.should == 200
|
85
|
+
last_response.body.should == ''
|
86
|
+
end
|
87
|
+
|
88
|
+
it """
|
89
|
+
DELETE /user/3
|
90
|
+
RETURN
|
91
|
+
- code: 200
|
92
|
+
- content-type: text/json
|
93
|
+
- body:
|
94
|
+
""" do
|
95
|
+
delete "/user/3"
|
96
|
+
last_response.status.should == 200
|
97
|
+
last_response.body.should == ''
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
module Repository
|
102
|
+
module User
|
103
|
+
@data = [{:user_id => 1, :username => 'one', :email => '1@one.com', :country => 'South Africa'},
|
104
|
+
{:user_id => 2, :username => 'two', :email => '2@two.com', :country => 'England'}]
|
105
|
+
|
106
|
+
def self.data
|
107
|
+
@data
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.one(id)
|
111
|
+
@data[@data.index{ |d| d[:user_id] == id.to_i}]
|
112
|
+
end
|
113
|
+
def self.all
|
114
|
+
@data
|
115
|
+
end
|
116
|
+
def self.new(data)
|
117
|
+
@data << data
|
118
|
+
end
|
119
|
+
def self.edit(id, data)
|
120
|
+
@data.each { |d| d = data if d[:user_id] == id}
|
121
|
+
end
|
122
|
+
def self.delete(id)
|
123
|
+
@data.delete_if { |i| i[:user_id] == id}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require './spec/spec_helper'
|
2
|
+
require './lib/ananke'
|
3
|
+
|
4
|
+
describe 'Resource' do
|
5
|
+
include Rack::Test::Methods
|
6
|
+
include Ananke
|
7
|
+
|
8
|
+
def app
|
9
|
+
Sinatra::Base
|
10
|
+
end
|
11
|
+
|
12
|
+
#--------------------------VALIDATION-----------------------------------
|
13
|
+
it """
|
14
|
+
Should be able to use Predefined Validation:
|
15
|
+
length
|
16
|
+
""" do
|
17
|
+
module Repository
|
18
|
+
module Basic
|
19
|
+
def self.new(data)end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
rest :basic do
|
23
|
+
id :user_id
|
24
|
+
required :username, :length => 4
|
25
|
+
end
|
26
|
+
|
27
|
+
post "/basic", body={:user_id => 1, :username => ''}
|
28
|
+
last_response.status.should == 400
|
29
|
+
last_response.body.should == 'username: Value must be at least 4 characters long'
|
30
|
+
|
31
|
+
post "/basic", body={:user_id => 1, :username => '1234'}
|
32
|
+
last_response.status.should == 201
|
33
|
+
end
|
34
|
+
|
35
|
+
it """
|
36
|
+
Should be able to use Explicitly Defined Rule
|
37
|
+
""" do
|
38
|
+
module Ananke
|
39
|
+
module Rules
|
40
|
+
def self.validate_email
|
41
|
+
value =~ /[\w._%-]+@[\w.-]+.[a-zA-Z]{2,4}/ ? nil : "Invalid Email: #{value}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
Ananke::Rules.respond_to?('validate_email').should == true
|
46
|
+
|
47
|
+
module Repository
|
48
|
+
module Explicit
|
49
|
+
def self.new(data)end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
rest :explicit do
|
53
|
+
id :user_id
|
54
|
+
required :email, :email
|
55
|
+
end
|
56
|
+
|
57
|
+
post "/explicit", body={:user_id => 1, :email => 'some'}
|
58
|
+
last_response.status.should == 400
|
59
|
+
last_response.body.should == 'email: Invalid Email: some'
|
60
|
+
|
61
|
+
post "/explicit", body={:user_id => 1, :email => 'some1@some.com'}
|
62
|
+
last_response.status.should == 201
|
63
|
+
end
|
64
|
+
|
65
|
+
it """
|
66
|
+
Should be able to Add new Validations and Use them
|
67
|
+
""" do
|
68
|
+
rule :country, do
|
69
|
+
value == 'South Africa' ? nil : 'Not from South Africa'
|
70
|
+
end
|
71
|
+
module Repository
|
72
|
+
module Added
|
73
|
+
def self.new(data)end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
rest :added do
|
77
|
+
id :user_id
|
78
|
+
required :country, :country
|
79
|
+
end
|
80
|
+
Ananke::Rules.respond_to?('validate_country').should == true
|
81
|
+
|
82
|
+
post "/added", body={:user_id => 1, :country => 'England'}
|
83
|
+
last_response.status.should == 400
|
84
|
+
last_response.body.should == 'country: Not from South Africa'
|
85
|
+
|
86
|
+
post "/added", body={:user_id => 1, :country => 'South Africa'}
|
87
|
+
last_response.status.should == 201
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,346 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'rspec/core/formatters/base_text_formatter'
|
3
|
+
require 'rspec/core/formatters/snippet_extractor'
|
4
|
+
|
5
|
+
class NiceFormatter < RSpec::Core::Formatters::BaseTextFormatter
|
6
|
+
include ERB::Util # for the #h method
|
7
|
+
|
8
|
+
def method_missing(m, *a, &b)
|
9
|
+
# no-op
|
10
|
+
end
|
11
|
+
|
12
|
+
def message(message)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(output)
|
16
|
+
super(output)
|
17
|
+
@example_group_number = 0
|
18
|
+
@example_number = 0
|
19
|
+
@header_red = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
# The number of the currently running example_group
|
23
|
+
def example_group_number
|
24
|
+
@example_group_number
|
25
|
+
end
|
26
|
+
|
27
|
+
# The number of the currently running example (a global counter)
|
28
|
+
def example_number
|
29
|
+
@example_number
|
30
|
+
end
|
31
|
+
|
32
|
+
def start(example_count)
|
33
|
+
super(example_count)
|
34
|
+
@output.puts html_header
|
35
|
+
@output.puts report_header
|
36
|
+
@output.flush
|
37
|
+
end
|
38
|
+
|
39
|
+
def example_group_started(example_group)
|
40
|
+
super(example_group)
|
41
|
+
@example_group_red = false
|
42
|
+
@example_group_number += 1
|
43
|
+
unless example_group_number == 1
|
44
|
+
@output.puts " </dl>"
|
45
|
+
@output.puts "</div>"
|
46
|
+
end
|
47
|
+
@output.puts "<div class=\"example_group\">"
|
48
|
+
@output.puts " <dl>"
|
49
|
+
@output.puts " <dt id=\"example_group_#{example_group_number}\">#{example_group.description.gsub(/[\n]/, "<br />")}</dt>"
|
50
|
+
@output.flush
|
51
|
+
end
|
52
|
+
|
53
|
+
def start_dump
|
54
|
+
@output.puts " </dl>"
|
55
|
+
@output.puts "</div>"
|
56
|
+
@output.flush
|
57
|
+
end
|
58
|
+
|
59
|
+
def example_started(example)
|
60
|
+
super(example)
|
61
|
+
@example_number += 1
|
62
|
+
end
|
63
|
+
|
64
|
+
def example_passed(example)
|
65
|
+
move_progress
|
66
|
+
@output.puts " <dd class=\"spec passed\"><span class=\"passed_spec_name\">#{example.description.gsub(/[\n]/, "<br />")}</span></dd>"
|
67
|
+
@output.flush
|
68
|
+
end
|
69
|
+
|
70
|
+
def example_failed(example)
|
71
|
+
counter = 0
|
72
|
+
exception = example.metadata[:execution_result][:exception_encountered]
|
73
|
+
extra = extra_failure_content(exception)
|
74
|
+
failure_style = 'failed'
|
75
|
+
failure_style = RSpec::Core::PendingExampleFixedError === exception ? 'pending_fixed' : 'failed'
|
76
|
+
@output.puts " <script type=\"text/javascript\">makeRed('rspec-header');</script>" unless @header_red
|
77
|
+
@header_red = true
|
78
|
+
@output.puts " <script type=\"text/javascript\">makeRed('example_group_#{example_group_number}');</script>" unless @example_group_red
|
79
|
+
@example_group_red = true
|
80
|
+
move_progress
|
81
|
+
@output.puts " <dd class=\"spec #{failure_style}\">"
|
82
|
+
@output.puts " <span class=\"failed_spec_name\">#{h(example.description.gsub(/[\n]/, "<br />"))}</span>"
|
83
|
+
@output.puts " <div class=\"failure\" id=\"failure_#{counter}\">"
|
84
|
+
@output.puts " <div class=\"message\"><pre>#{h(exception.message)}</pre></div>" unless exception.nil?
|
85
|
+
@output.puts " <div class=\"backtrace\"><pre>#{format_backtrace(exception.backtrace, example).join("\n")}</pre></div>" if exception
|
86
|
+
@output.puts extra unless extra == ""
|
87
|
+
@output.puts " </div>"
|
88
|
+
@output.puts " </dd>"
|
89
|
+
@output.flush
|
90
|
+
end
|
91
|
+
|
92
|
+
def example_pending(example)
|
93
|
+
message = example.metadata[:execution_result][:pending_message]
|
94
|
+
@output.puts " <script type=\"text/javascript\">makeYellow('rspec-header');</script>" unless @header_red
|
95
|
+
@output.puts " <script type=\"text/javascript\">makeYellow('example_group_#{example_group_number}');</script>" unless @example_group_red
|
96
|
+
move_progress
|
97
|
+
@output.puts " <dd class=\"spec not_implemented\"><span class=\"not_implemented_spec_name\">#{example.description.gsub(/[\n]/, "<br />")} (PENDING: #{h(message)})</span></dd>"
|
98
|
+
@output.flush
|
99
|
+
end
|
100
|
+
|
101
|
+
# Override this method if you wish to output extra HTML for a failed spec. For example, you
|
102
|
+
# could output links to images or other files produced during the specs.
|
103
|
+
#
|
104
|
+
def extra_failure_content(exception)
|
105
|
+
require 'rspec/core/formatters/snippet_extractor'
|
106
|
+
@snippet_extractor ||= RSpec::Core::Formatters::SnippetExtractor.new
|
107
|
+
" <pre class=\"ruby\"><code>#{@snippet_extractor.snippet(exception)}</code></pre>"
|
108
|
+
end
|
109
|
+
|
110
|
+
def move_progress
|
111
|
+
@output.puts " <script type=\"text/javascript\">moveProgressBar('#{percent_done}');</script>"
|
112
|
+
@output.flush
|
113
|
+
end
|
114
|
+
|
115
|
+
def percent_done
|
116
|
+
result = 100.0
|
117
|
+
if @example_count > 0
|
118
|
+
result = ((example_number).to_f / @example_count.to_f * 1000).to_i / 10.0
|
119
|
+
end
|
120
|
+
result
|
121
|
+
end
|
122
|
+
|
123
|
+
def dump_failures
|
124
|
+
end
|
125
|
+
|
126
|
+
def dump_pending
|
127
|
+
end
|
128
|
+
|
129
|
+
def dump_summary(duration, example_count, failure_count, pending_count)
|
130
|
+
# TODO - kill dry_run?
|
131
|
+
if dry_run?
|
132
|
+
totals = "This was a dry-run"
|
133
|
+
else
|
134
|
+
totals = "#{example_count} example#{'s' unless example_count == 1}, #{failure_count} failure#{'s' unless failure_count == 1}"
|
135
|
+
totals << ", #{pending_count} pending" if pending_count > 0
|
136
|
+
end
|
137
|
+
@output.puts "<script type=\"text/javascript\">document.getElementById('duration').innerHTML = \"Finished in <strong>#{duration} seconds</strong>\";</script>"
|
138
|
+
@output.puts "<script type=\"text/javascript\">document.getElementById('totals').innerHTML = \"#{totals}\";</script>"
|
139
|
+
@output.puts "</div>"
|
140
|
+
@output.puts "</div>"
|
141
|
+
@output.puts "</body>"
|
142
|
+
@output.puts "</html>"
|
143
|
+
@output.flush
|
144
|
+
end
|
145
|
+
|
146
|
+
def html_header
|
147
|
+
<<-EOF
|
148
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
149
|
+
<!DOCTYPE html
|
150
|
+
PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
151
|
+
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
152
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
153
|
+
<head>
|
154
|
+
<title>RSpec results</title>
|
155
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
156
|
+
<meta http-equiv="Expires" content="-1" />
|
157
|
+
<meta http-equiv="Pragma" content="no-cache" />
|
158
|
+
<style type="text/css">
|
159
|
+
body {
|
160
|
+
margin: 0;
|
161
|
+
padding: 0;
|
162
|
+
background: #fff;
|
163
|
+
font-size: 80%;
|
164
|
+
}
|
165
|
+
</style>
|
166
|
+
<script type="text/javascript">
|
167
|
+
// <![CDATA[
|
168
|
+
#{global_scripts}
|
169
|
+
// ]]>
|
170
|
+
</script>
|
171
|
+
<style type="text/css">
|
172
|
+
#{global_styles}
|
173
|
+
</style>
|
174
|
+
</head>
|
175
|
+
<body>
|
176
|
+
EOF
|
177
|
+
end
|
178
|
+
|
179
|
+
def report_header
|
180
|
+
<<-EOF
|
181
|
+
<div class="rspec-report">
|
182
|
+
|
183
|
+
<div id="rspec-header">
|
184
|
+
<div id="label">
|
185
|
+
<h1>RSpec Code Examples</h1>
|
186
|
+
</div>
|
187
|
+
|
188
|
+
<div id="summary">
|
189
|
+
<p id="totals"> </p>
|
190
|
+
<p id="duration"> </p>
|
191
|
+
</div>
|
192
|
+
</div>
|
193
|
+
|
194
|
+
<div class="results">
|
195
|
+
EOF
|
196
|
+
end
|
197
|
+
|
198
|
+
def global_scripts
|
199
|
+
<<-EOF
|
200
|
+
function moveProgressBar(percentDone) {
|
201
|
+
document.getElementById("rspec-header").style.width = percentDone +"%";
|
202
|
+
}
|
203
|
+
function makeRed(element_id) {
|
204
|
+
document.getElementById(element_id).style.background = '#C40D0D';
|
205
|
+
document.getElementById(element_id).style.color = '#FFFFFF';
|
206
|
+
}
|
207
|
+
|
208
|
+
function makeYellow(element_id) {
|
209
|
+
if (element_id == "rspec-header" && document.getElementById(element_id).style.background != '#C40D0D')
|
210
|
+
{
|
211
|
+
document.getElementById(element_id).style.background = '#FAF834';
|
212
|
+
document.getElementById(element_id).style.color = '#000000';
|
213
|
+
}
|
214
|
+
else
|
215
|
+
{
|
216
|
+
document.getElementById(element_id).style.background = '#FAF834';
|
217
|
+
document.getElementById(element_id).style.color = '#000000';
|
218
|
+
}
|
219
|
+
}
|
220
|
+
EOF
|
221
|
+
end
|
222
|
+
|
223
|
+
def global_styles
|
224
|
+
<<-EOF
|
225
|
+
#rspec-header {
|
226
|
+
background: #65C400; color: #fff; height: 4em;
|
227
|
+
}
|
228
|
+
|
229
|
+
.rspec-report h1 {
|
230
|
+
margin: 0px 10px 0px 10px;
|
231
|
+
padding: 10px;
|
232
|
+
font-family: "Lucida Grande", Helvetica, sans-serif;
|
233
|
+
font-size: 1.8em;
|
234
|
+
position: absolute;
|
235
|
+
}
|
236
|
+
|
237
|
+
#summary {
|
238
|
+
margin: 0; padding: 5px 10px;
|
239
|
+
font-family: "Lucida Grande", Helvetica, sans-serif;
|
240
|
+
text-align: right;
|
241
|
+
top: 0px;
|
242
|
+
right: 0px;
|
243
|
+
float:right;
|
244
|
+
}
|
245
|
+
|
246
|
+
#summary p {
|
247
|
+
margin: 0 0 0 2px;
|
248
|
+
}
|
249
|
+
|
250
|
+
#summary #totals {
|
251
|
+
font-size: 1.2em;
|
252
|
+
}
|
253
|
+
|
254
|
+
.example_group {
|
255
|
+
margin: 0 10px 5px;
|
256
|
+
background: #fff;
|
257
|
+
}
|
258
|
+
|
259
|
+
dl {
|
260
|
+
margin: 0; padding: 0 0 5px;
|
261
|
+
font: normal 11px "Lucida Grande", Helvetica, sans-serif;
|
262
|
+
}
|
263
|
+
|
264
|
+
dt {
|
265
|
+
padding: 3px;
|
266
|
+
background: #65C400;
|
267
|
+
color: #fff;
|
268
|
+
font-weight: bold;
|
269
|
+
}
|
270
|
+
|
271
|
+
dd {
|
272
|
+
margin: 5px 0 5px 5px;
|
273
|
+
padding: 3px 3px 3px 18px;
|
274
|
+
}
|
275
|
+
|
276
|
+
dd.spec.passed {
|
277
|
+
border-left: 5px solid #65C400;
|
278
|
+
border-bottom: 1px solid #65C400;
|
279
|
+
background: #DBFFB4; color: #3D7700;
|
280
|
+
}
|
281
|
+
|
282
|
+
dd.spec.failed {
|
283
|
+
border-left: 5px solid #C20000;
|
284
|
+
border-bottom: 1px solid #C20000;
|
285
|
+
color: #C20000; background: #FFFBD3;
|
286
|
+
}
|
287
|
+
|
288
|
+
dd.spec.not_implemented {
|
289
|
+
border-left: 5px solid #FAF834;
|
290
|
+
border-bottom: 1px solid #FAF834;
|
291
|
+
background: #FCFB98; color: #131313;
|
292
|
+
}
|
293
|
+
|
294
|
+
dd.spec.pending_fixed {
|
295
|
+
border-left: 5px solid #0000C2;
|
296
|
+
border-bottom: 1px solid #0000C2;
|
297
|
+
color: #0000C2; background: #D3FBFF;
|
298
|
+
}
|
299
|
+
|
300
|
+
.backtrace {
|
301
|
+
color: #000;
|
302
|
+
font-size: 12px;
|
303
|
+
}
|
304
|
+
|
305
|
+
a {
|
306
|
+
color: #BE5C00;
|
307
|
+
}
|
308
|
+
|
309
|
+
/* Ruby code, style similar to vibrant ink */
|
310
|
+
.ruby {
|
311
|
+
font-size: 12px;
|
312
|
+
font-family: monospace;
|
313
|
+
color: white;
|
314
|
+
background-color: black;
|
315
|
+
padding: 0.1em 0 0.2em 0;
|
316
|
+
}
|
317
|
+
|
318
|
+
.ruby .keyword { color: #FF6600; }
|
319
|
+
.ruby .constant { color: #339999; }
|
320
|
+
.ruby .attribute { color: white; }
|
321
|
+
.ruby .global { color: white; }
|
322
|
+
.ruby .module { color: white; }
|
323
|
+
.ruby .class { color: white; }
|
324
|
+
.ruby .string { color: #66FF00; }
|
325
|
+
.ruby .ident { color: white; }
|
326
|
+
.ruby .method { color: #FFCC00; }
|
327
|
+
.ruby .number { color: white; }
|
328
|
+
.ruby .char { color: white; }
|
329
|
+
.ruby .comment { color: #9933CC; }
|
330
|
+
.ruby .symbol { color: white; }
|
331
|
+
.ruby .regex { color: #44B4CC; }
|
332
|
+
.ruby .punct { color: white; }
|
333
|
+
.ruby .escape { color: white; }
|
334
|
+
.ruby .interp { color: white; }
|
335
|
+
.ruby .expr { color: white; }
|
336
|
+
|
337
|
+
.ruby .offending { background-color: gray; }
|
338
|
+
.ruby .linenum {
|
339
|
+
width: 75px;
|
340
|
+
padding: 0.1em 1em 0.2em 0;
|
341
|
+
color: #000000;
|
342
|
+
background-color: #FFFBD3;
|
343
|
+
}
|
344
|
+
EOF
|
345
|
+
end
|
346
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
#=========================CODE COVERAGE========================
|
2
|
+
require './spec/cov_adapter'
|
3
|
+
SimpleCov.start 'cov'
|
4
|
+
|
5
|
+
#===========================REQUIRES===========================
|
6
|
+
require 'colored'
|
7
|
+
require 'json'
|
8
|
+
require 'rack'
|
9
|
+
require 'rspec'
|
10
|
+
require 'rack/test'
|
11
|
+
|
12
|
+
extend Colored
|
13
|
+
|
14
|
+
#==================SETUP TEST ENVIRONMENT======================
|
15
|
+
ENV['RACK_ENV'] = 'test'
|
16
|
+
|
17
|
+
$LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), ".."))
|
metadata
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ananke
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Andries Coetzee
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-02-07 00:00:00 +02:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: sinatra
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ~>
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: 1.1.2
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: colored
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ~>
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: "1.2"
|
36
|
+
type: :development
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: json
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 1.5.1
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: rack-test
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ~>
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: 0.5.6
|
58
|
+
type: :development
|
59
|
+
version_requirements: *id004
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: rake
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.8.7
|
69
|
+
type: :development
|
70
|
+
version_requirements: *id005
|
71
|
+
- !ruby/object:Gem::Dependency
|
72
|
+
name: rspec
|
73
|
+
prerelease: false
|
74
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ~>
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 2.5.0
|
80
|
+
type: :development
|
81
|
+
version_requirements: *id006
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: simplecov
|
84
|
+
prerelease: false
|
85
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ~>
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: 0.3.9
|
91
|
+
type: :development
|
92
|
+
version_requirements: *id007
|
93
|
+
description: Full REST Implementation on top of Sinatra
|
94
|
+
email: andriesc@mixtel.com
|
95
|
+
executables: []
|
96
|
+
|
97
|
+
extensions: []
|
98
|
+
|
99
|
+
extra_rdoc_files:
|
100
|
+
- README.rdoc
|
101
|
+
files:
|
102
|
+
- lib/ananke.rb
|
103
|
+
- lib/version.rb
|
104
|
+
- spec/cov_adapter.rb
|
105
|
+
- spec/lib/ananke_spec.rb
|
106
|
+
- spec/lib/validation_spec.rb
|
107
|
+
- spec/spec_helper.rb
|
108
|
+
- spec/nice_formatter.rb
|
109
|
+
- Gemfile
|
110
|
+
- Rakefile
|
111
|
+
- README.rdoc
|
112
|
+
has_rdoc: true
|
113
|
+
homepage: https://github.com/HasAndries/ananke
|
114
|
+
licenses: []
|
115
|
+
|
116
|
+
post_install_message: |
|
117
|
+
**************************************************
|
118
|
+
|
119
|
+
Thank you for installing ananke-0.0.1
|
120
|
+
|
121
|
+
Please be sure to look at README.rdoc to see what might have changed
|
122
|
+
since the last release and how to use this GEM.
|
123
|
+
|
124
|
+
**************************************************
|
125
|
+
|
126
|
+
rdoc_options:
|
127
|
+
- --charset=UTF-8
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
none: false
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: "0"
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ">="
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: "0"
|
142
|
+
requirements: []
|
143
|
+
|
144
|
+
rubyforge_project:
|
145
|
+
rubygems_version: 1.5.0
|
146
|
+
signing_key:
|
147
|
+
specification_version: 3
|
148
|
+
summary: ananke-0.0.1
|
149
|
+
test_files:
|
150
|
+
- spec/cov_adapter.rb
|
151
|
+
- spec/lib/ananke_spec.rb
|
152
|
+
- spec/lib/validation_spec.rb
|
153
|
+
- spec/spec_helper.rb
|
154
|
+
- spec/nice_formatter.rb
|