ares-ext 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .DS_Store
2
+ *.log
3
+ pkg/*
4
+ coverage/*
5
+ doc/*
6
+ benchmarks/*
7
+ .bundle
8
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,20 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ active_resource_extensions (0.0.1)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ activeresource (2.3.5)
10
+ activesupport (= 2.3.5)
11
+ activesupport (2.3.5)
12
+ rspec (1.3.2)
13
+
14
+ PLATFORMS
15
+ ruby
16
+
17
+ DEPENDENCIES
18
+ active_resource_extensions!
19
+ activeresource (= 2.3.5)
20
+ rspec (~> 1.3.1)
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ active_resource_extensions
2
+ ==========================
3
+
4
+ Add useful behaviors to ActiveResource models
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ Bundler.setup
4
+ #require 'appraisal'
5
+
6
+ require 'spec/rake/spectask'
7
+ Spec::Rake::SpecTask.new(:spec) do |spec|
8
+ spec.libs << 'lib' << 'spec'
9
+ spec.spec_files = FileList['spec/**/*_spec.rb']
10
+ end
11
+
12
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
13
+ spec.libs << 'lib' << 'spec'
14
+ spec.pattern = 'spec/**/*_spec.rb'
15
+ spec.rcov = true
16
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.authors = ["Benoît Dinocourt"]
5
+ gem.email = ["ghrind@gmail.com"]
6
+ gem.description = "Make activeresource model compatible with will_paginate and searchlogic helpers, and add a schema feature"
7
+ gem.summary = gem.description
8
+ gem.homepage = "http://github.com/Ghrind/active_resource_extensions"
9
+
10
+ gem.files = `git ls-files`.split($\)
11
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
12
+ gem.name = "ares-ext"
13
+ gem.require_paths = ["lib"]
14
+ gem.version = '0.0.1'
15
+
16
+ #gem.extra_rdoc_files = [
17
+ # "LICENSE",
18
+ # "README.rdoc"
19
+ #]
20
+ # gem.date = %q{2011-08-15}
21
+
22
+ gem.add_development_dependency 'rspec', '~> 1.3.1'
23
+ gem.add_development_dependency 'activeresource', '2.3.5'
24
+ end
@@ -0,0 +1,69 @@
1
+ module ActiveResourceExtensions
2
+ # Get schema from webservice, detect missing attributes and cast known attributes appropriately
3
+ module ResourceWithSchema
4
+
5
+ def self.included base
6
+ base.extend ClassMethods
7
+ base.send :include, InstanceMethods
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ def is_attribute? name
13
+ schema.keys.include? name.to_s
14
+ end
15
+
16
+ def attribute_type name
17
+ schema[name.to_s]
18
+ end
19
+
20
+ protected
21
+
22
+ def schema
23
+ @schema ||= {}
24
+ end
25
+
26
+ def schema= new_schema
27
+ @schema = new_schema
28
+ end
29
+
30
+ def load_remote_schema
31
+ self.schema = get( :schema )
32
+ end
33
+
34
+ end
35
+
36
+ module InstanceMethods
37
+ def method_missing name, *args, &block
38
+ if self.class.is_attribute? name
39
+ value = attributes[name.to_s]
40
+ return if value.nil?
41
+ if self.class.is_attribute? name
42
+ case self.class.attribute_type name
43
+ when 'Time'
44
+ Time.parse value
45
+ when 'Integer'
46
+ value.to_i
47
+ when 'Float'
48
+ value.to_f
49
+ when 'Boolean'
50
+ [true, 1, '1'].include? value
51
+ when ''
52
+ value
53
+ else
54
+ begin
55
+ self.class.attribute_type( name ).constantize.new value
56
+ rescue ArgumentError, NameError
57
+ raise ArgumentError, "Can't cast #{value.inspect} into #{self.class.attribute_type( name )}"
58
+ end
59
+ end
60
+ else
61
+ return value
62
+ end
63
+ else
64
+ super name, *args, &block
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,44 @@
1
+ module ActiveResourceExtensions
2
+
3
+ module SearchableResource
4
+
5
+ # Proxy of an Array compatible with will_paginate
6
+ class Collection
7
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ }
8
+
9
+ attr_reader :current_page, :total_entries, :per_page
10
+
11
+ def initialize target, current_page, per_page, total_entries
12
+ @target = target
13
+ @current_page = current_page
14
+ @per_page = per_page
15
+ @total_entries = total_entries
16
+ end
17
+
18
+ def total_pages
19
+ ( total_entries.to_f / @per_page ).ceil
20
+ end
21
+
22
+ def previous_page
23
+ current_page == 1 ? nil : ( current_page - 1 )
24
+ end
25
+
26
+ def next_page
27
+ current_page == total_pages ? nil : ( current_page + 1 )
28
+ end
29
+
30
+ def target
31
+ @target ||= []
32
+ end
33
+
34
+ protected
35
+
36
+ def method_missing(name, *args, &block)
37
+ target.send(name, *args, &block)
38
+ end
39
+
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,61 @@
1
+ module ActiveResourceExtensions
2
+
3
+ module SearchableResource
4
+
5
+ class Search
6
+
7
+ DEFAULT_PAGE = 1
8
+ DEFAULT_PER_PAGE = 10
9
+
10
+ attr_reader :conditions
11
+
12
+ def initialize model, conditions = {}
13
+ @model = model
14
+ @conditions = conditions
15
+ end
16
+
17
+ def paginate pagination_options = {}
18
+ # Get the parameters corresponding to the search options
19
+ params = parse_conditions @conditions
20
+
21
+ total_entries = @model.count params
22
+
23
+ # Manage pagination
24
+ params[:page] = ( pagination_options[:page] || DEFAULT_PAGE ).to_i
25
+ params[:per_page] = ( pagination_options[:per_page] || DEFAULT_PER_PAGE ).to_i
26
+
27
+ SearchableResource::Collection.new @model.find( :all, :params => params ), params[:page], params[:per_page], total_entries
28
+ end
29
+
30
+ protected
31
+
32
+ # TODO We can add custom search options here
33
+ def parse_conditions conditions
34
+
35
+ # By default all empty options are removed
36
+ params = conditions.delete_if{ |k, v| v.blank? }
37
+
38
+ params
39
+ end
40
+
41
+ # If
42
+ def method_missing(name, *args, &block)
43
+ name.to_s =~ /^([\w]*)(=?)$/
44
+ attr = $1.to_sym
45
+
46
+ ignore_attribute = ! @model.respond_to?( :is_attribute? )
47
+
48
+ if ignore_attribute or @model.is_attribute?( attr )
49
+ if $2 == '='
50
+ @conditions[attr]= args.first
51
+ else
52
+ @conditions[attr]
53
+ end
54
+ else
55
+ super name, *args, &block
56
+ end
57
+ end
58
+
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,22 @@
1
+ module ActiveResourceExtensions
2
+ # Add search and pagination features to ActiveResource models
3
+ module SearchableResource
4
+
5
+ def self.included base
6
+ base.extend ClassMethods
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ def search_for options = {}
12
+ SearchableResource::Search.new self, options
13
+ end
14
+
15
+ def count params = {}
16
+ get( :count, params )['count'].to_i
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,4 @@
1
+ require 'active_resource_extensions/resource_with_schema'
2
+ require 'active_resource_extensions/searchable_resource'
3
+ require 'active_resource_extensions/searchable_resource/search'
4
+ require 'active_resource_extensions/searchable_resource/collection'
@@ -0,0 +1,168 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveResourceExtensions::ResourceWithSchema do
4
+
5
+ before do
6
+ class Person < ActiveResource::Base
7
+ self.site = 'http://test.webservicehost.com'
8
+ include ActiveResourceExtensions::ResourceWithSchema
9
+ end
10
+ end
11
+
12
+ after do
13
+ class ::Object
14
+ remove_const :Person
15
+ end
16
+ end
17
+
18
+ it "should add a schema method" do
19
+ Person.respond_to?( :schema ).should be_true
20
+ end
21
+
22
+ it "should have an empty schema" do
23
+ Person.send( :schema ).should == {}
24
+ end
25
+
26
+ it "should replace schema" do
27
+ schema = { 'name' => 'String' }
28
+ Person.send( :schema=, schema )
29
+ Person.send( :schema ).should == schema
30
+ end
31
+
32
+ describe '::load_remote_schema' do
33
+ it "should use active_resource get method" do
34
+ Person.should_receive( :get ).with( :schema )
35
+ Person.send :load_remote_schema
36
+ end
37
+
38
+ it "should load schema from response" do
39
+ schema = { 'name' => 'String', 'age' => 'Integer' }
40
+ Person.should_receive( :get ).with( :schema ).and_return( schema )
41
+ Person.send :load_remote_schema
42
+ Person.send( :schema ).should == schema
43
+ end
44
+ end
45
+
46
+ describe '::is_attribute?' do
47
+
48
+ before do
49
+ Person.send( :schema=, { 'name' => 'String', 'age' => 'Integer' } )
50
+ end
51
+
52
+ it "should find attribute when name is a symbol" do
53
+ Person.is_attribute?( :name ).should be_true
54
+ end
55
+
56
+ it "should find attribute when name is a string" do
57
+ Person.is_attribute?( 'name' ).should be_true
58
+ end
59
+
60
+ it "should return false if attribute doesn't exists" do
61
+ Person.is_attribute?( 'firstname' ).should be_false
62
+ end
63
+ end
64
+
65
+ describe '::attribute_type?' do
66
+
67
+ before do
68
+ Person.send( :schema=, { 'name' => 'String', 'age' => 'Integer' } )
69
+ end
70
+
71
+ it "should return attribute's type when name is a string" do
72
+ Person.attribute_type( 'name' ).should == 'String'
73
+ end
74
+
75
+ it "should return attribute's type when name is a symbol" do
76
+ Person.attribute_type( :name ).should == 'String'
77
+ end
78
+
79
+ it "should return nil if attribute is unknown" do
80
+ Person.attribute_type( :firstname ).should == nil
81
+ end
82
+ end
83
+
84
+ describe '.method_missing' do
85
+
86
+ before do
87
+ Person.send( :schema=, {
88
+ 'name' => 'String',
89
+ 'age' => 'Integer',
90
+ 'created_at' => 'Time',
91
+ 'is_musician' => 'Boolean',
92
+ 'height' => 'Float',
93
+ 'unknown_1' => 'Company',
94
+ 'unknown_2' => ''
95
+ } )
96
+ end
97
+
98
+ context "when method is not an attribute name" do
99
+
100
+ before do
101
+ @person = Person.new
102
+ end
103
+
104
+ it "should have default behavior" do
105
+ lambda do
106
+ @person.firstname
107
+ end.should raise_error NoMethodError
108
+ end
109
+ end
110
+
111
+ context "when method is an attribute name" do
112
+
113
+ context "when attribute has not been loaded" do
114
+
115
+ before do
116
+ @person = Person.new
117
+ end
118
+
119
+ it "should return nil" do
120
+ @person.name.should be_nil
121
+ end
122
+ end
123
+
124
+ context "when attribute has been loaded" do
125
+
126
+ before do
127
+ @person = Person.new(
128
+ :name => 'John',
129
+ :age => '26',
130
+ :height => '5.3',
131
+ :is_musician => '1',
132
+ :created_at => '2012-05-01 23:15:12',
133
+ :unknown_1 => 'a',
134
+ :unknown_2 => 'b' )
135
+ end
136
+
137
+ it "should cast value appropriately" do
138
+ @person.name.should == 'John'
139
+ @person.age.should == 26
140
+ @person.height.should == 5.3
141
+ @person.is_musician.should == true
142
+ @person.created_at.should == Time.parse('2012-05-01 23:15:12')
143
+
144
+ for value in [ '0', 0, false, 'true' ]
145
+ person = Person.new( :is_musician => value )
146
+ if person.is_musician != false
147
+ violated "#{value.inspect} should be cast as false"
148
+ end
149
+ end
150
+
151
+ end
152
+
153
+ it "should raise an ArgumentError if casting cannot be made" do
154
+ lambda do
155
+ @person.unknown_1
156
+ end.should raise_error ArgumentError
157
+ end
158
+
159
+ it "should return value if attribute type is an empty string" do
160
+ @person.unknown_2.should == 'b'
161
+ end
162
+ end
163
+
164
+ end
165
+
166
+ end
167
+
168
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveResourceExtensions::SearchableResource::Collection do
4
+
5
+ before do
6
+ @collection = ActiveResourceExtensions::SearchableResource::Collection.new [], 2, 10, 51
7
+ end
8
+
9
+ describe '.total_pages' do
10
+ it "should return correct page number" do
11
+ @collection.total_pages.should == 6
12
+ end
13
+ end
14
+
15
+ describe '.previous_page' do
16
+
17
+ context "when at the first page" do
18
+
19
+ before do
20
+ @collection = ActiveResourceExtensions::SearchableResource::Collection.new [], 1, 10, 51
21
+ end
22
+
23
+ it "should return nil" do
24
+ @collection.previous_page.should be_nil
25
+ end
26
+
27
+ end
28
+
29
+ context "when not at the first page" do
30
+
31
+ it "should return correct page number" do
32
+ @collection.previous_page.should == 1
33
+ end
34
+
35
+ end
36
+ end
37
+
38
+ describe '.next_page' do
39
+ context "when at the last page" do
40
+
41
+ before do
42
+ @collection = ActiveResourceExtensions::SearchableResource::Collection.new [], 6, 10, 51
43
+ end
44
+
45
+ it "should return nil" do
46
+ @collection.next_page.should be_nil
47
+ end
48
+
49
+ end
50
+
51
+ context "when not at the last page" do
52
+
53
+ it "should return correct page number" do
54
+ @collection.next_page.should == 3
55
+ end
56
+
57
+ end
58
+ end
59
+
60
+ end
@@ -0,0 +1,124 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveResourceExtensions::SearchableResource::Search do
4
+
5
+ before do
6
+ class Person < ActiveResource::Base
7
+ self.site = 'http://test.webservicehost.com'
8
+ include ActiveResourceExtensions::SearchableResource
9
+ end
10
+ @search = Person.search_for
11
+ end
12
+
13
+ after do
14
+ class ::Object
15
+ remove_const :Person
16
+ end
17
+ end
18
+
19
+ describe '.method_missing' do
20
+ context "when class has a schema" do
21
+
22
+ before do
23
+ Person.send :include, ActiveResourceExtensions::ResourceWithSchema
24
+ Person.send :schema=, 'name' => 'String', 'age' => 'Integer'
25
+ @search = Person.search_for
26
+ end
27
+
28
+ context "when method name is a known attribute" do
29
+
30
+ it "should set condition" do
31
+ @search.age = 26
32
+ @search.conditions[:age].should == 26
33
+ end
34
+
35
+ it "should get condition" do
36
+ @search.instance_variable_set :@conditions, :age => 26
37
+ @search.age.should == 26
38
+ end
39
+
40
+ end
41
+
42
+ context "when method name is not an attribute" do
43
+ it "should have default behavior" do
44
+ lambda do
45
+ @search.firstname
46
+ end.should raise_error NoMethodError
47
+
48
+ lambda do
49
+ @search.firstname= 'bob'
50
+ end.should raise_error NoMethodError
51
+ end
52
+ end
53
+ end
54
+
55
+ context "when class has no schema" do
56
+ it "should set condition" do
57
+ @search.firstname = 'bob'
58
+ @search.conditions[:firstname].should == 'bob'
59
+ end
60
+ it "should get condition" do
61
+ @search.firstname.should be_nil
62
+ end
63
+ end
64
+ end
65
+
66
+ describe '.parse_conditions' do
67
+ it "should remove empty conditions" do
68
+ @search.send( :parse_conditions, :name_like => 'john', :age => '' ).should == { :name_like => 'john' }
69
+ end
70
+ end
71
+
72
+ describe '.paginate' do
73
+
74
+ it "should parse conditions" do
75
+ @search.name_like = 'john'
76
+ @search.age = ''
77
+
78
+ @search.should_receive( :parse_conditions ).with( :name_like => 'john', :age => '' ).and_return :name_like => 'john'
79
+
80
+ Person.should_receive( :count).with( { :name_like => 'john' } ).and_return 0
81
+ Person.should_receive( :find ).with( :all, :params => { :page => 1, :per_page => 10, :name_like => 'john' } ).and_return []
82
+
83
+ @search.paginate
84
+ end
85
+
86
+ it "should set collection 'current_page' attribute" do
87
+ Person.should_receive( :count).with( {} ).and_return 0
88
+ Person.should_receive( :find ).with( :all, :params => { :page => 5, :per_page => 10 } ).and_return []
89
+ @search.paginate( :page => 5 ).current_page.should == 5
90
+ end
91
+
92
+ it "should set collection 'per_page' attribute" do
93
+ Person.should_receive( :count).with( {} ).and_return 0
94
+ Person.should_receive( :find ).with( :all, :params => { :page => 1, :per_page => 100 } ).and_return []
95
+ @search.paginate( :per_page => 100 ).per_page.should == 100
96
+ end
97
+
98
+ describe "returned object" do
99
+
100
+ before do
101
+ @dataset = [Person.new, Person.new]
102
+ Person.should_receive( :count).with( anything ).and_return 2
103
+ Person.should_receive( :find ).with( :all, anything ).and_return @dataset
104
+ end
105
+
106
+ it "should use default if 'page' option is missing" do
107
+ @search.paginate.current_page.should == 1
108
+ end
109
+
110
+ it "should use default if 'per_page' option is missing" do
111
+ @search.paginate.per_page.should == 10
112
+ end
113
+
114
+ it "should set collection 'target' attribute" do
115
+ @search.paginate.target.should == @dataset
116
+ end
117
+
118
+ it "should set collection 'total_entries' attribute" do
119
+ @search.paginate.total_entries.should == 2
120
+ end
121
+ end
122
+ end
123
+
124
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveResourceExtensions::SearchableResource do
4
+
5
+ before do
6
+ class Person < ActiveResource::Base
7
+ self.site = 'http://test.webservicehost.com'
8
+ include ActiveResourceExtensions::SearchableResource
9
+ end
10
+ end
11
+
12
+ after do
13
+ class ::Object
14
+ remove_const :Person
15
+ end
16
+ end
17
+
18
+ describe '#search_for' do
19
+ it "should return an instance of ActiveResourceExtensions::SearchableResource::Search" do
20
+ Person.search_for.class.name.should == "ActiveResourceExtensions::SearchableResource::Search"
21
+ end
22
+ end
23
+
24
+ describe '#count' do
25
+ it "should return an integer with the count" do
26
+ Person.should_receive( :get ).with( :count, {} ).and_return({ 'count' => '3' })
27
+ Person.count.should == 3
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,3 @@
1
+ Bundler.setup
2
+ require 'active_resource'
3
+ require 'active_resource_extensions'
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ares-ext
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - "Beno\xC3\xAEt Dinocourt"
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-05-23 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: rspec
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 25
30
+ segments:
31
+ - 1
32
+ - 3
33
+ - 1
34
+ version: 1.3.1
35
+ type: :development
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: activeresource
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - "="
44
+ - !ruby/object:Gem::Version
45
+ hash: 9
46
+ segments:
47
+ - 2
48
+ - 3
49
+ - 5
50
+ version: 2.3.5
51
+ type: :development
52
+ version_requirements: *id002
53
+ description: Make activeresource model compatible with will_paginate and searchlogic helpers, and add a schema feature
54
+ email:
55
+ - ghrind@gmail.com
56
+ executables: []
57
+
58
+ extensions: []
59
+
60
+ extra_rdoc_files: []
61
+
62
+ files:
63
+ - .gitignore
64
+ - Gemfile
65
+ - Gemfile.lock
66
+ - README.md
67
+ - Rakefile
68
+ - active_resource_extensions.gemspec
69
+ - lib/active_resource_extensions.rb
70
+ - lib/active_resource_extensions/resource_with_schema.rb
71
+ - lib/active_resource_extensions/searchable_resource.rb
72
+ - lib/active_resource_extensions/searchable_resource/collection.rb
73
+ - lib/active_resource_extensions/searchable_resource/search.rb
74
+ - spec/active_resource_extensions/resource_with_schema_spec.rb
75
+ - spec/active_resource_extensions/searchable_resource_spec.rb
76
+ - spec/active_resource_extensions/searchable_resource_spec/collection_spec.rb
77
+ - spec/active_resource_extensions/searchable_resource_spec/search_spec.rb
78
+ - spec/spec_helper.rb
79
+ has_rdoc: true
80
+ homepage: http://github.com/Ghrind/active_resource_extensions
81
+ licenses: []
82
+
83
+ post_install_message:
84
+ rdoc_options: []
85
+
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ hash: 3
94
+ segments:
95
+ - 0
96
+ version: "0"
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ hash: 3
103
+ segments:
104
+ - 0
105
+ version: "0"
106
+ requirements: []
107
+
108
+ rubyforge_project:
109
+ rubygems_version: 1.6.1
110
+ signing_key:
111
+ specification_version: 3
112
+ summary: Make activeresource model compatible with will_paginate and searchlogic helpers, and add a schema feature
113
+ test_files:
114
+ - spec/active_resource_extensions/resource_with_schema_spec.rb
115
+ - spec/active_resource_extensions/searchable_resource_spec.rb
116
+ - spec/active_resource_extensions/searchable_resource_spec/collection_spec.rb
117
+ - spec/active_resource_extensions/searchable_resource_spec/search_spec.rb
118
+ - spec/spec_helper.rb