rsolr 0.13.0.pre → 1.0.0.beta

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/lib/rsolr/xml.rb ADDED
@@ -0,0 +1,161 @@
1
+ module RSolr::Xml
2
+
3
+ class Document
4
+
5
+ # "attrs" is a hash for setting the "doc" xml attributes
6
+ # "fields" is an array of Field objects
7
+ attr_accessor :attrs, :fields
8
+
9
+ # "doc_hash" must be a Hash/Mash object
10
+ # If a value in the "doc_hash" is an array,
11
+ # a field object is created for each value...
12
+ def initialize(doc_hash = {})
13
+ @fields = []
14
+ doc_hash.each_pair do |field,values|
15
+ # create a new field for each value (multi-valued)
16
+ # put non-array values into an array
17
+ values = [values] unless values.is_a?(Array)
18
+ values.each do |v|
19
+ next if v.to_s.empty?
20
+ @fields << RSolr::Xml::Field.new({:name=>field}, v.to_s)
21
+ end
22
+ end
23
+ @attrs={}
24
+ end
25
+
26
+ # returns an array of fields that match the "name" arg
27
+ def fields_by_name(name)
28
+ @fields.select{|f|f.name==name}
29
+ end
30
+
31
+ # returns the *first* field that matches the "name" arg
32
+ def field_by_name(name)
33
+ @fields.detect{|f|f.name==name}
34
+ end
35
+
36
+ #
37
+ # Add a field value to the document. Options map directly to
38
+ # XML attributes in the Solr <field> node.
39
+ # See http://wiki.apache.org/solr/UpdateXmlMessages#head-8315b8028923d028950ff750a57ee22cbf7977c6
40
+ #
41
+ # === Example:
42
+ #
43
+ # document.add_field('title', 'A Title', :boost => 2.0)
44
+ #
45
+ def add_field(name, value, options = {})
46
+ @fields << RSolr::Xml::Field.new(options.merge({:name=>name}), value)
47
+ end
48
+
49
+ end
50
+
51
+ class Field
52
+
53
+ # "attrs" is a hash for setting the "doc" xml attributes
54
+ # "value" is the text value for the node
55
+ attr_accessor :attrs, :value
56
+
57
+ # "attrs" must be a hash
58
+ # "value" should be something that responds to #_to_s
59
+ def initialize(attrs, value)
60
+ @attrs = attrs
61
+ @value = value
62
+ end
63
+
64
+ # the value of the "name" attribute
65
+ def name
66
+ @attrs[:name]
67
+ end
68
+
69
+ end
70
+
71
+ class Generator
72
+
73
+ def build &block
74
+ require 'builder'
75
+ b = ::Builder::XmlMarkup.new(:indent=>0, :margin=>0, :encoding => 'UTF-8')
76
+ b.instruct!
77
+ block_given? ? yield(b) : b
78
+ end
79
+
80
+ # generates "add" xml for updating solr
81
+ # "data" can be a hash or an array of hashes.
82
+ # - each hash should be a simple key=>value pair representing a solr doc.
83
+ # If a value is an array, multiple fields will be created.
84
+ #
85
+ # "add_attrs" can be a hash for setting the add xml element attributes.
86
+ #
87
+ # This method can also accept a block.
88
+ # The value yielded to the block is a Message::Document; for each solr doc in "data".
89
+ # You can set xml element attributes for each "doc" element or individual "field" elements.
90
+ #
91
+ # For example:
92
+ #
93
+ # solr.add({:id=>1, :nickname=>'Tim'}, {:boost=>5.0, :commitWithin=>1.0}) do |doc_msg|
94
+ # doc_msg.attrs[:boost] = 10.00 # boost the document
95
+ # nickname = doc_msg.field_by_name(:nickname)
96
+ # nickname.attrs[:boost] = 20 if nickname.value=='Tim' # boost a field
97
+ # end
98
+ #
99
+ # would result in an add element having the attributes boost="10.0"
100
+ # and a commitWithin="1.0".
101
+ # Each doc element would have a boost="10.0".
102
+ # The "nickname" field would have a boost="20.0"
103
+ # if the doc had a "nickname" field with the value of "Tim".
104
+ #
105
+ def add data, add_attrs={}, &block
106
+ data = [data] unless data.is_a?(Array)
107
+ build do |xml|
108
+ xml.add(add_attrs) do |add_node|
109
+ data.each do |doc|
110
+ doc = RSolr::Xml::Document.new(doc) if doc.respond_to?(:each_pair)
111
+ yield doc if block_given?
112
+ add_node.doc(doc.attrs) do |doc_node|
113
+ doc.fields.each do |field_obj|
114
+ doc_node.field field_obj.value, field_obj.attrs
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ # generates a <commit/> message
123
+ def commit(opts={})
124
+ build {|xml| xml.commit opts}
125
+ end
126
+
127
+ # generates a <optimize/> message
128
+ def optimize(opts={})
129
+ build {|xml| xml.optimize opts}
130
+ end
131
+
132
+ # generates a <rollback/> message
133
+ def rollback opts={}
134
+ build {|xml| xml.rollback opts}
135
+ end
136
+
137
+ # generates a <delete><id>ID</id></delete> message
138
+ # "ids" can be a single value or array of values
139
+ def delete_by_id(ids)
140
+ ids = [ids] unless ids.is_a?(Array)
141
+ build do |xml|
142
+ xml.delete do |delete_node|
143
+ ids.each { |id| delete_node.id(id) }
144
+ end
145
+ end
146
+ end
147
+
148
+ # generates a <delete><query>ID</query></delete> message
149
+ # "queries" can be a single value or an array of values
150
+ def delete_by_query(queries)
151
+ queries = [queries] unless queries.is_a?(Array)
152
+ build do |xml|
153
+ xml.delete do |delete_node|
154
+ queries.each { |query| delete_node.query query }
155
+ end
156
+ end
157
+ end
158
+
159
+ end
160
+
161
+ end
data/lib/rsolr.rb CHANGED
@@ -1,53 +1,32 @@
1
-
1
+ require 'uri'
2
+ require 'net/http'
3
+ require 'net/https'
2
4
  require 'rubygems'
3
- $:.unshift File.dirname(__FILE__) unless $:.include?(File.dirname(__FILE__))
5
+ require 'builder'
6
+
7
+ $: << "#{File.dirname(__FILE__)}"
4
8
 
5
9
  module RSolr
6
10
 
11
+ %W(Char Client Error Http Uri Xml).each{|n|autoload n.to_sym, "rsolr/#{n.downcase}"}
12
+
7
13
  def self.version
8
- @version ||= File.read(File.join(File.dirname(__FILE__), '..', 'VERSION'))
14
+ @version ||= File.read(File.join(File.dirname(__FILE__), '..', 'VERSION')).chomp
9
15
  end
10
16
 
11
17
  VERSION = self.version
12
18
 
13
- autoload :Message, 'rsolr/message'
14
- autoload :Client, 'rsolr/client'
15
- autoload :Connection, 'rsolr/connection'
16
-
17
- module Connectable
18
-
19
- def connect opts={}
20
- Client.new Connection::NetHttp.new(opts)
21
- end
22
-
19
+ def self.connect *args
20
+ opts = parse_options *args
21
+ Client.new Http.new(opts[0], opts[1])
23
22
  end
24
23
 
25
- extend Connectable
26
-
27
- # A module that contains string related methods
28
- module Char
29
-
30
- # escape - from the solr-ruby library
31
- # RSolr.escape('asdf')
32
- # backslash everything that isn't a word character
33
- def escape(value)
34
- value.gsub(/(\W)/, '\\\\\1')
35
- end
36
-
24
+ def self.parse_options *args
25
+ opts = args[-1].kind_of?(Hash) ? args.pop : {}
26
+ url = args.empty? ? 'http://127.0.0.1:8983/solr/' : args[0]
27
+ uri = Uri.create url
28
+ proxy = Uri.create opts[:proxy] if opts[:proxy]
29
+ [uri, {:proxy => proxy}]
37
30
  end
38
31
 
39
- # send the escape method into the Connection class ->
40
- # solr = RSolr.connect
41
- # solr.escape('asdf')
42
- RSolr::Client.send(:include, Char)
43
-
44
- # bring escape into this module (RSolr) -> RSolr.escape('asdf')
45
- extend Char
46
-
47
- # RequestError is a common/generic exception class used by the adapters
48
- class RequestError < RuntimeError; end
49
-
50
- # TODO: The connection drivers need to start raising this...
51
- class ConnectionError < RuntimeError; end
52
-
53
32
  end
@@ -0,0 +1,195 @@
1
+ require 'spec_helper'
2
+ describe "RSolr::Client" do
3
+
4
+ module ClientHelper
5
+ def client
6
+ @client ||= (
7
+ connection = RSolr::Http.new URI.parse("http://localhost:9999/solr")
8
+ RSolr::Client.new(connection)
9
+ )
10
+ end
11
+ end
12
+
13
+ context "initialize" do
14
+ it "should accept whatevs and set it as the @connection" do
15
+ RSolr::Client.new(:whatevs).connection.should == :whatevs
16
+ end
17
+ end
18
+
19
+ context "adapt_response" do
20
+
21
+ include ClientHelper
22
+
23
+ it 'should not try to evaluate ruby when the :qt is not :ruby' do
24
+ body = '{:time=>"NOW"}'
25
+ result = client.send(:adapt_response, {:params=>{}}, {:body => body})
26
+ result.should be_a(String)
27
+ result.should == body
28
+ end
29
+
30
+ it 'should evaluate ruby responses when the :wt is :ruby' do
31
+ body = '{:time=>"NOW"}'
32
+ result = client.send(:adapt_response, {:params=>{:wt=>:ruby}}, {:body=>body})
33
+ result.should be_a(Hash)
34
+ result.should == {:time=>"NOW"}
35
+ end
36
+
37
+ ["nil", :ruby].each do |wt|
38
+ it "should return an object that responds to :request and :response when :wt == #{wt}" do
39
+ req = {:params=>{:wt=>wt}}
40
+ res = {:body=>""}
41
+ result = client.send(:adapt_response, req, res)
42
+ result.request.should == req
43
+ result.response.should == res
44
+ end
45
+ end
46
+
47
+ it "ought raise a RSolr::Error::InvalidRubyResponse when the ruby is indeed frugged" do
48
+ lambda {
49
+ client.send(:adapt_response, {:params=>{:wt => :ruby}}, {:body => "<woops/>"})
50
+ }.should raise_error RSolr::Error::InvalidRubyResponse
51
+ end
52
+
53
+ end
54
+
55
+ context "build_request" do
56
+ include ClientHelper
57
+ it 'should return a request context array' do
58
+ result = client.build_request 'select', {:q=>'test', :fq=>[0,1]}, "data", headers = {}
59
+ ["select?fq=0&fq=1&q=test", "select?q=test&fq=0&fq=1"].should include(result[0].to_s)
60
+ result[1].should == "data"
61
+ result[2].should == headers
62
+ end
63
+ it "should set the Content-Type header to application/x-www-form-urlencoded if a hash is passed in to the data arg" do
64
+ result = client.build_request 'select', nil, {:q=>'test', :fq=>[0,1]}, headers = {}
65
+ result[0].to_s.should == "select"
66
+ ["fq=0&fq=1&q=test", "q=test&fq=0&fq=1"].should include(result[1])
67
+ result[2].should == headers
68
+ end
69
+ end
70
+
71
+ context "map_params" do
72
+ include ClientHelper
73
+ it "should return a hash if nil is passed in" do
74
+ client.map_params(nil).should == {:wt => :ruby}
75
+ end
76
+ it "should set the :wt to ruby if blank" do
77
+ r = client.map_params({:q=>"q"})
78
+ r[:q].should == "q"
79
+ r[:wt].should == :ruby
80
+ end
81
+ it "should not override the :wt to ruby if set" do
82
+ r = client.map_params({:q=>"q", :wt => :json})
83
+ r[:q].should == "q"
84
+ r[:wt].should == :json
85
+ end
86
+ end
87
+
88
+ context "send_request" do
89
+ include ClientHelper
90
+ it "should forward these method calls the #connection object" do
91
+ [:get, :post, :head].each do |meth|
92
+ client.connection.should_receive(meth).
93
+ and_return({:status => 200})
94
+ client.send_request meth, '', {}, nil, {}
95
+ end
96
+ end
97
+ it "should extend any exception raised by the #connection object with a RSolr::Error::SolrContext" do
98
+ client.connection.should_receive(:get).
99
+ and_raise(RuntimeError)
100
+ lambda {
101
+ client.send_request :get, '', {}, nil, {}
102
+ }.should raise_error(RuntimeError){|error|
103
+ error.should be_a(RSolr::Error::SolrContext)
104
+ error.should respond_to(:request)
105
+ error.request.keys.should include(:connection, :method, :uri, :data, :headers, :params)
106
+ }
107
+ end
108
+ it "should raise an Http error if the response status code aint right" do
109
+ client.connection.should_receive(:get).
110
+ and_return({:status_code => 404})
111
+ lambda{
112
+ client.send_request :get, '', {}, nil, {}
113
+ }.should raise_error(RSolr::Error::Http) {|error|
114
+ error.should be_a(RSolr::Error::Http)
115
+ error.should respond_to(:request)
116
+ error.should respond_to(:response)
117
+ }
118
+ end
119
+ end
120
+
121
+ context "post" do
122
+ include ClientHelper
123
+ it "should pass the expected params to the connection's #post method" do
124
+ client.connection.should_receive(:post).
125
+ with("update?wt=ruby", "the data", {"Content-Type" => "text/plain"}).
126
+ and_return(:status => 200)
127
+ client.post "update", "the data", nil, {"Content-Type" => "text/plain"}
128
+ end
129
+ end
130
+
131
+ context "xml" do
132
+ include ClientHelper
133
+ it "should return an instance of RSolr::Xml::Generator" do
134
+ client.xml.should be_a RSolr::Xml::Generator
135
+ end
136
+ end
137
+
138
+ context "add" do
139
+ include ClientHelper
140
+ it "should send xml to the connection's #post method" do
141
+ client.connection.should_receive(:post).
142
+ with("update?wt=ruby", "<xml/>", {"Content-Type"=>"text/xml"}).
143
+ and_return(:status => 200)
144
+ # the :xml attr is lazy loaded... so load it up first
145
+ client.xml
146
+ client.xml.should_receive(:add).
147
+ with({:id=>1}, :commitWith=>1.0).
148
+ and_return("<xml/>")
149
+ client.add({:id=>1}, {:commitWith=>1.0})
150
+ end
151
+ end
152
+
153
+ context "update" do
154
+ include ClientHelper
155
+ it "should send data to the connection's #post method" do
156
+ client.connection.should_receive(:post).
157
+ with("update?wt=xml", instance_of(String), {"Content-Type"=>"text/xml"}).
158
+ and_return(:status => 200)
159
+ client.update("<optimize/>", {:wt=>:xml})
160
+ end
161
+ end
162
+
163
+ context "post based helper methods:" do
164
+ include ClientHelper
165
+ [:commit, :optimize, :rollback].each do |meth|
166
+ it "should send a #{meth} message to the connection's #post method" do
167
+ client.connection.should_receive(:post).
168
+ with("update?wt=ruby", "<?xml version=\"1.0\" encoding=\"UTF-8\"?><#{meth}/>", {"Content-Type"=>"text/xml"}).
169
+ and_return(:status => 200)
170
+ client.send meth
171
+ end
172
+ end
173
+ end
174
+
175
+ context "delete_by_id" do
176
+ include ClientHelper
177
+ it "should send data to the connection's #post method" do
178
+ client.connection.should_receive(:post).
179
+ with("update?wt=ruby", instance_of(String), {"Content-Type"=>"text/xml"}).
180
+ and_return(:status => 200)
181
+ client.delete_by_id 1
182
+ end
183
+ end
184
+
185
+ context "delete_by_query" do
186
+ include ClientHelper
187
+ it "should send data to the connection's #post method" do
188
+ client.connection.should_receive(:post).
189
+ with("update?wt=ruby", instance_of(String), {"Content-Type"=>"text/xml"}).
190
+ and_return(:status => 200)
191
+ client.delete_by_query :fq => "category:\"trash\""
192
+ end
193
+ end
194
+
195
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+ describe "RSolr::Error" do
3
+ # add some shiz here...
4
+ end
@@ -0,0 +1,4 @@
1
+ require 'spec_helper'
2
+ describe "RSolr::Http" do
3
+ # add some shiz here...
4
+ end
@@ -1,29 +1,17 @@
1
- describe RSolr do
1
+ require 'spec_helper'
2
+ describe "RSolr class methods" do
2
3
 
3
- context :connect do
4
-
5
- it 'does not care about valid/live URLs yet' do
6
- lambda{RSolr.connect :url=>'http://blah.blah.blah:666/solr'}.should_not raise_error
7
- end
8
-
9
- it 'should create an instance of RSolr::Connection::NetHttp as the #connection' do
10
- expected_class = RSolr::Connection::NetHttp
11
- RSolr.connect.connection.should be_a(expected_class)
12
- RSolr.connect(:url=>'blah').connection.should be_a(expected_class)
13
- end
14
-
4
+ it "should parse these here options" do
5
+ result = RSolr.parse_options "http://localhost:8983/solr/blah", :proxy => "http://qtpaglzvm.com"
6
+ result[0].should be_a(URI)
7
+ result[1].should be_a(Hash)
8
+ result[1][:proxy].should be_a(URI)
15
9
  end
16
10
 
17
- context :escape do
18
-
19
- it "should escape properly" do
20
- RSolr.escape('Trying & % different "characters" here!').should == "Trying\\ \\&\\ \\%\\ different\\ \\\"characters\\\"\\ here\\!"
21
- end
22
-
23
- it 'should escape' do
24
- expected = "http\\:\\/\\/lucene\\.apache\\.org\\/solr"
25
- RSolr.escape("http://lucene.apache.org/solr").should == expected
26
- end
11
+ it "should not create a URI instance for :proxy => nil" do
12
+ result = RSolr.parse_options "http://localhost:8983/solr/blah"
13
+ result[0].should be_a(URI)
14
+ result[1].should == {:proxy => nil}
27
15
  end
28
16
 
29
17
  end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+ describe "RSolr::Uri" do
3
+
4
+ context "class-level methods" do
5
+
6
+ let(:uri){ RSolr::Uri }
7
+
8
+ it "should return a URI object with a trailing slash" do
9
+ u = uri.create 'http://apache.org'
10
+ u.path[0].should == ?/
11
+ end
12
+
13
+ it "should return the bytesize of a string" do
14
+ uri.bytesize("test").should == 4
15
+ end
16
+
17
+ it "should convert a solr query string from a hash w/o a starting ?" do
18
+ hash = {:q => "gold", :fq => ["mode:one", "level:2"]}
19
+ query = uri.params_to_solr hash
20
+ query[0].should_not == ??
21
+ [/q=gold/, /fq=mode%3Aone/, /fq=level%3A2/].each do |p|
22
+ query.should match p
23
+ end
24
+ query.split('&').size.should == 3
25
+ end
26
+
27
+ context "escape_query_value" do
28
+
29
+ it 'should escape &' do
30
+ uri.params_to_solr(:fq => "&").should == 'fq=%26'
31
+ end
32
+
33
+ it 'should convert spaces to +' do
34
+ uri.params_to_solr(:fq => "me and you").should == 'fq=me+and+you'
35
+ end
36
+
37
+ it 'should escape comlex queries, part 1' do
38
+ my_params = {'fq' => '{!raw f=field_name}crazy+\"field+value'}
39
+ expected = 'fq=%7B%21raw+f%3Dfield_name%7Dcrazy%2B%5C%22field%2Bvalue'
40
+ uri.params_to_solr(my_params).should == expected
41
+ end
42
+
43
+ it 'should escape complex queries, part 2' do
44
+ my_params = {'q' => '+popularity:[10 TO *] +section:0'}
45
+ expected = 'q=%2Bpopularity%3A%5B10+TO+%2A%5D+%2Bsection%3A0'
46
+ uri.params_to_solr(my_params).should == expected
47
+ end
48
+
49
+ it 'should escape properly' do
50
+ uri.escape_query_value('+').should == '%2B'
51
+ uri.escape_query_value('This is a test').should == 'This+is+a+test'
52
+ uri.escape_query_value('<>/\\').should == '%3C%3E%2F%5C'
53
+ uri.escape_query_value('"').should == '%22'
54
+ uri.escape_query_value(':').should == '%3A'
55
+ end
56
+
57
+ it 'should escape brackets' do
58
+ uri.escape_query_value('{').should == '%7B'
59
+ uri.escape_query_value('}').should == '%7D'
60
+ end
61
+
62
+ it 'should escape exclamation marks!' do
63
+ uri.escape_query_value('!').should == '%21'
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -1,6 +1,7 @@
1
- describe RSolr::Message do
1
+ require 'spec_helper'
2
+ describe "RSolr::Xml" do
2
3
 
3
- let(:generator){ RSolr::Message::Generator.new }
4
+ let(:generator){ RSolr::Xml::Generator.new }
4
5
 
5
6
  # call all of the simple methods...
6
7
  # make sure the xml string is valid
@@ -70,7 +71,7 @@ describe RSolr::Message do
70
71
  end
71
72
 
72
73
  it 'should create an add from a single Message::Document' do
73
- document = RSolr::Message::Document.new
74
+ document = RSolr::Xml::Document.new
74
75
  document.add_field('id', 1)
75
76
  document.add_field('name', 'matt', :boost => 2.0)
76
77
  result = generator.add(document)
@@ -83,7 +84,7 @@ describe RSolr::Message do
83
84
 
84
85
  it 'should create adds from multiple Message::Documents' do
85
86
  documents = (1..2).map do |i|
86
- doc = RSolr::Message::Document.new
87
+ doc = RSolr::Xml::Document.new
87
88
  doc.add_field('id', i)
88
89
  doc.add_field('name', "matt#{i}")
89
90
  doc
metadata CHANGED
@@ -3,11 +3,11 @@ name: rsolr
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease: true
5
5
  segments:
6
+ - 1
6
7
  - 0
7
- - 13
8
8
  - 0
9
- - pre
10
- version: 0.13.0.pre
9
+ - beta
10
+ version: 1.0.0.beta
11
11
  platform: ruby
12
12
  authors:
13
13
  - Matt Mitchell
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-04-04 00:00:00 -04:00
18
+ date: 2010-06-29 00:00:00 -04:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -46,15 +46,12 @@ files:
46
46
  - README.rdoc
47
47
  - VERSION
48
48
  - lib/rsolr.rb
49
+ - lib/rsolr/char.rb
49
50
  - lib/rsolr/client.rb
50
- - lib/rsolr/connection.rb
51
- - lib/rsolr/connection/httpable.rb
52
- - lib/rsolr/connection/net_http.rb
53
- - lib/rsolr/connection/utils.rb
54
- - lib/rsolr/message.rb
55
- - lib/rsolr/message/document.rb
56
- - lib/rsolr/message/field.rb
57
- - lib/rsolr/message/generator.rb
51
+ - lib/rsolr/error.rb
52
+ - lib/rsolr/http.rb
53
+ - lib/rsolr/uri.rb
54
+ - lib/rsolr/xml.rb
58
55
  has_rdoc: true
59
56
  homepage: http://github.com/mwmitchell/rsolr
60
57
  licenses: []
@@ -88,13 +85,12 @@ signing_key:
88
85
  specification_version: 3
89
86
  summary: A Ruby client for Apache Solr
90
87
  test_files:
91
- - spec/api/client_spec.rb
92
- - spec/api/connection/httpable_spec.rb
93
- - spec/api/connection/net_http_spec.rb
94
- - spec/api/connection/utils_spec.rb
95
- - spec/api/message_spec.rb
88
+ - spec/api/rsolr_client_spec.rb
89
+ - spec/api/rsolr_error_spec.rb
90
+ - spec/api/rsolr_http_spec.rb
96
91
  - spec/api/rsolr_spec.rb
97
- - spec/integration/http_errors_spec.rb
92
+ - spec/api/rsolr_uri_spec.rb
93
+ - spec/api/rsolr_xml_spec.rb
98
94
  - spec/spec_helper.rb
99
95
  - Rakefile
100
96
  - tasks/spec.rake