ananke 0.0.1
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/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
|