caring-aws-sdb 0.3.2.caring.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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2007, 2008 Tim Dysinger
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README ADDED
@@ -0,0 +1 @@
1
+ Amazon SDB API
data/Rakefile ADDED
@@ -0,0 +1,57 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "aws-sdb"
8
+ gem.summary = "Amazon SDB API"
9
+ gem.description = gem.summary
10
+ gem.email = "ddollar@gmail.com"
11
+ gem.homepage = "http://github.com/ddollar/aws-sdb"
12
+ gem.authors = ["Tim Dysinger", "David Dollar"]
13
+ gem.add_runtime_dependency "uuidtools"
14
+ gem.add_development_dependency 'rspec'
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/*_test.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/*_test.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ if File.exist?('VERSION')
48
+ version = File.read('VERSION')
49
+ else
50
+ version = ""
51
+ end
52
+
53
+ rdoc.rdoc_dir = 'rdoc'
54
+ rdoc.title = "gem-github-stats #{version}"
55
+ rdoc.rdoc_files.include('README*')
56
+ rdoc.rdoc_files.include('lib/**/*.rb')
57
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.1
data/aws-sdb.gemspec ADDED
@@ -0,0 +1,56 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{caring-aws-sdb}
8
+ s.version = "0.3.2.caring.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Tim Dysinger", "David Dollar"]
12
+ s.date = %q{2009-10-02}
13
+ s.description = %q{Amazon SDB API}
14
+ s.email = %q{ddollar@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README"
18
+ ]
19
+ s.files = [
20
+ "LICENSE",
21
+ "README",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "aws-sdb.gemspec",
25
+ "lib/aws_sdb.rb",
26
+ "lib/aws_sdb/error.rb",
27
+ "lib/aws_sdb/service.rb",
28
+ "spec/aws_sdb/service_spec.rb",
29
+ "spec/spec_helper.rb"
30
+ ]
31
+ s.homepage = %q{http://github.com/ddollar/aws-sdb}
32
+ s.rdoc_options = ["--charset=UTF-8"]
33
+ s.require_paths = ["lib"]
34
+ s.rubygems_version = %q{1.3.5}
35
+ s.summary = %q{Amazon SDB API}
36
+ s.test_files = [
37
+ "spec/aws_sdb/service_spec.rb",
38
+ "spec/spec_helper.rb"
39
+ ]
40
+
41
+ if s.respond_to? :specification_version then
42
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
43
+ s.specification_version = 3
44
+
45
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
46
+ s.add_runtime_dependency(%q<uuidtools>, [">= 0"])
47
+ s.add_development_dependency(%q<rspec>, [">= 0"])
48
+ else
49
+ s.add_dependency(%q<uuidtools>, [">= 0"])
50
+ s.add_dependency(%q<rspec>, [">= 0"])
51
+ end
52
+ else
53
+ s.add_dependency(%q<uuidtools>, [">= 0"])
54
+ s.add_dependency(%q<rspec>, [">= 0"])
55
+ end
56
+ end
data/lib/aws_sdb.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'aws_sdb/error'
2
+ require 'aws_sdb/service'
3
+
@@ -0,0 +1,42 @@
1
+ module AwsSdb
2
+
3
+ class Error < RuntimeError ; end
4
+
5
+ class RequestError < Error
6
+ attr_reader :request_id
7
+
8
+ def initialize(message, request_id=nil)
9
+ super(message)
10
+ @request_id = request_id
11
+ end
12
+ end
13
+
14
+ class InvalidDomainNameError < RequestError ; end
15
+ class InvalidParameterValueError < RequestError ; end
16
+ class InvalidNextTokenError < RequestError ; end
17
+ class InvalidNumberPredicatesError < RequestError ; end
18
+ class InvalidNumberValueTestsError < RequestError ; end
19
+ class InvalidQueryExpressionError < RequestError ; end
20
+ class MissingParameterError < RequestError ; end
21
+ class NoSuchDomainError < RequestError ; end
22
+ class NumberDomainsExceededError < RequestError ; end
23
+ class NumberDomainAttributesExceededError < RequestError ; end
24
+ class NumberDomainBytesExceededError < RequestError ; end
25
+ class NumberItemAttributesExceededError < RequestError ; end
26
+ class RequestTimeoutError < RequestError ; end
27
+
28
+ class FeatureDeprecatedError < RequestError ; end
29
+
30
+ class ConnectionError < Error
31
+ attr_reader :response
32
+
33
+ def initialize(response)
34
+ super(
35
+ "#{response.code} \
36
+ #{response.message if response.respond_to?(:message)}"
37
+ )
38
+ @response = response
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,183 @@
1
+ require 'logger'
2
+ require 'time'
3
+ require 'cgi'
4
+ require 'uri'
5
+ require 'net/http'
6
+ require 'base64'
7
+ require 'openssl'
8
+ require 'rexml/document'
9
+ require 'rexml/xpath'
10
+
11
+ module AwsSdb
12
+
13
+ class Service
14
+ def initialize(options={})
15
+ @access_key_id = options[:access_key_id] || ENV['AMAZON_ACCESS_KEY_ID']
16
+ @secret_access_key = options[:secret_access_key] || ENV['AMAZON_SECRET_ACCESS_KEY']
17
+ @base_url = options[:url] || 'http://sdb.amazonaws.com'
18
+ @logger = options[:logger] || Logger.new("aws_sdb.log")
19
+ end
20
+
21
+ def list_domains(max = nil, token = nil)
22
+ params = { 'Action' => 'ListDomains' }
23
+ params['NextToken'] =
24
+ token unless token.nil? || token.empty?
25
+ params['MaxNumberOfDomains'] =
26
+ max.to_s unless max.nil? || max.to_i == 0
27
+ doc = call(:get, params)
28
+ results = []
29
+ REXML::XPath.each(doc, '//DomainName/text()') do |domain|
30
+ results << domain.to_s
31
+ end
32
+ return results, REXML::XPath.first(doc, '//NextToken/text()').to_s
33
+ end
34
+
35
+ def create_domain(domain)
36
+ call(:post, { 'Action' => 'CreateDomain', 'DomainName'=> domain.to_s })
37
+ nil
38
+ end
39
+
40
+ def delete_domain(domain)
41
+ call(
42
+ :delete,
43
+ { 'Action' => 'DeleteDomain', 'DomainName' => domain.to_s }
44
+ )
45
+ nil
46
+ end
47
+
48
+ def query(domain, query, max = nil, token = nil)
49
+ params = {
50
+ 'Action' => 'Query',
51
+ 'QueryExpression' => query,
52
+ 'DomainName' => domain.to_s
53
+ }
54
+ params['NextToken'] =
55
+ token unless token.nil? || token.empty?
56
+ params['MaxNumberOfItems'] =
57
+ max.to_s unless max.nil? || max.to_i == 0
58
+ doc = call(:get, params)
59
+ results = []
60
+ REXML::XPath.each(doc, '//ItemName/text()') do |item|
61
+ results << item.to_s
62
+ end
63
+ return results, REXML::XPath.first(doc, '//NextToken/text()').to_s
64
+ end
65
+
66
+ def query_with_attributes(domain, query, max = nil, token = nil)
67
+ params = {
68
+ 'Action' => 'QueryWithAttributes',
69
+ 'QueryExpression' => query,
70
+ 'DomainName' => domain.to_s
71
+ }
72
+ params['NextToken'] =
73
+ token unless token.nil? || token.empty?
74
+ params['MaxNumberOfItems'] =
75
+ max.to_s unless max.nil? || max.to_i == 0
76
+ doc = call(:get, params)
77
+ results = []
78
+ REXML::XPath.each(doc, '//Item') do |item_element|
79
+ item = { :name => item_element.get_text('Name').to_s }
80
+ attributes = {}
81
+ item_element.each_element('Attribute') do |attribute|
82
+ attributes[attribute.get_text('Name').to_s] = attribute.get_text('Value').to_s
83
+ end
84
+ item[:attributes] = attributes
85
+ results << item
86
+ end
87
+ return results, REXML::XPath.first(doc, '//NextToken/text()').to_s
88
+ end
89
+
90
+ def put_attributes(domain, item, attributes, replace = true)
91
+ params = {
92
+ 'Action' => 'PutAttributes',
93
+ 'DomainName' => domain.to_s,
94
+ 'ItemName' => item.to_s
95
+ }
96
+ count = 0
97
+ attributes.each do | key, values |
98
+ ([]<<values).flatten.each do |value|
99
+ params["Attribute.#{count}.Name"] = key.to_s
100
+ params["Attribute.#{count}.Value"] = value.to_s
101
+ params["Attribute.#{count}.Replace"] = replace
102
+ count += 1
103
+ end
104
+ end
105
+ call(:put, params)
106
+ nil
107
+ end
108
+
109
+ def get_attributes(domain, item)
110
+ doc = call(
111
+ :get,
112
+ {
113
+ 'Action' => 'GetAttributes',
114
+ 'DomainName' => domain.to_s,
115
+ 'ItemName' => item.to_s
116
+ }
117
+ )
118
+ attributes = {}
119
+ REXML::XPath.each(doc, "//Attribute") do |attr|
120
+ key = REXML::XPath.first(attr, './Name/text()').to_s
121
+ value = REXML::XPath.first(attr, './Value/text()').to_s
122
+ ( attributes[key] ||= [] ) << value
123
+ end
124
+ attributes
125
+ end
126
+
127
+ def delete_attributes(domain, item)
128
+ call(
129
+ :delete,
130
+ {
131
+ 'Action' => 'DeleteAttributes',
132
+ 'DomainName' => domain.to_s,
133
+ 'ItemName' => item.to_s
134
+ }
135
+ )
136
+ nil
137
+ end
138
+
139
+ protected
140
+
141
+ def call(method, params)
142
+ params.merge!( {
143
+ 'Version' => '2007-11-07',
144
+ 'SignatureVersion' => '1',
145
+ 'AWSAccessKeyId' => @access_key_id,
146
+ 'Timestamp' => Time.now.gmtime.iso8601
147
+ }
148
+ )
149
+ data = ''
150
+ query = []
151
+ params.keys.sort_by { |k| k.upcase }.each do |key|
152
+ data << "#{key}#{params[key].to_s}"
153
+ query << "#{key}=#{CGI::escape(params[key].to_s)}"
154
+ end
155
+ digest = OpenSSL::Digest::Digest.new('sha1')
156
+ hmac = OpenSSL::HMAC.digest(digest, @secret_access_key, data)
157
+ signature = Base64.encode64(hmac).strip
158
+ query << "Signature=#{CGI::escape(signature)}"
159
+ query = query.join('&')
160
+ url = "#{@base_url}?#{query}"
161
+ uri = URI.parse(url)
162
+ @logger.debug("#{url}") if @logger
163
+ response =
164
+ Net::HTTP.new(uri.host, uri.port).send_request(method, uri.request_uri)
165
+ @logger.debug("#{response.code}\n#{response.body}") if @logger
166
+ raise(ConnectionError.new(response)) unless (200..400).include?(
167
+ response.code.to_i
168
+ )
169
+ doc = REXML::Document.new(response.body)
170
+ error = doc.get_elements('*/Errors/Error')[0]
171
+ raise(
172
+ Module.class_eval(
173
+ "AwsSdb::#{error.get_elements('Code')[0].text}Error"
174
+ ).new(
175
+ error.get_elements('Message')[0].text,
176
+ doc.get_elements('*/RequestID')[0].text
177
+ )
178
+ ) unless error.nil?
179
+ doc
180
+ end
181
+ end
182
+
183
+ end
@@ -0,0 +1,185 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper.rb'
2
+
3
+ require 'digest/sha1'
4
+ require 'net/http'
5
+ require 'rexml/document'
6
+
7
+ require 'rubygems'
8
+ require 'uuidtools'
9
+
10
+ include AwsSdb
11
+
12
+ describe Service, "when creating a new domain" do
13
+ before(:all) do
14
+ @service = AwsSdb::Service.new
15
+ @domain = "test-#{UUID.random_create.to_s}"
16
+ domains = @service.list_domains[0]
17
+ domains.each do |d|
18
+ @service.delete_domain(d) if d =~ /^test/
19
+ end
20
+ end
21
+
22
+ after(:all) do
23
+ @service.delete_domain(@domain)
24
+ end
25
+
26
+ it "should not raise an error if a valid new domain name is given" do
27
+ lambda {
28
+ @service.create_domain("test-#{UUID.random_create.to_s}")
29
+ }.should_not raise_error
30
+ end
31
+
32
+ it "should not raise an error if the domain name already exists" do
33
+ domain = "test-#{UUID.random_create.to_s}"
34
+ lambda {
35
+ @service.create_domain(domain)
36
+ @service.create_domain(domain)
37
+ }.should_not raise_error
38
+ end
39
+
40
+ it "should raise an error if an a nil or '' domain name is given" do
41
+ lambda {
42
+ @service.create_domain('')
43
+ }.should raise_error(InvalidParameterValueError)
44
+ lambda {
45
+ @service.create_domain(nil)
46
+ }.should raise_error(InvalidParameterValueError)
47
+ lambda {
48
+ @service.create_domain(' ')
49
+ }.should raise_error(InvalidParameterValueError)
50
+ end
51
+
52
+ it "should raise an error if the domain name length is < 3 or > 255" do
53
+ lambda {
54
+ @service.create_domain('xx')
55
+ }.should raise_error(InvalidParameterValueError)
56
+ lambda {
57
+ @service.create_domain('x'*256)
58
+ }.should raise_error(InvalidParameterValueError)
59
+ end
60
+
61
+ it "should only accept domain names with a-z, A-Z, 0-9, '_', '-', and '.' " do
62
+ lambda {
63
+ @service.create_domain('@$^*()')
64
+ }.should raise_error(InvalidParameterValueError)
65
+ end
66
+
67
+ it "should only accept a maximum of 100 domain names" do
68
+ # TODO Implement this example
69
+ end
70
+
71
+ it "should not have to call amazon to determine domain name correctness" do
72
+ # TODO Implement this example
73
+ end
74
+ end
75
+
76
+ describe Service, "when listing domains" do
77
+ before(:all) do
78
+ @service = AwsSdb::Service.new
79
+ @domain = "test-#{UUID.random_create.to_s}"
80
+ @service.list_domains[0].each do |d|
81
+ @service.delete_domain(d) if d =~ /^test/
82
+ end
83
+ @service.create_domain(@domain)
84
+ end
85
+
86
+ after(:all) do
87
+ @service.delete_domain(@domain)
88
+ end
89
+
90
+ it "should return a complete list" do
91
+ result = nil
92
+ lambda { result = @service.list_domains[0] }.should_not raise_error
93
+ result.should_not be_nil
94
+ result.should_not be_empty
95
+ result.include?(@domain).should == true
96
+ end
97
+ end
98
+
99
+ describe Service, "when deleting domains" do
100
+ before(:all) do
101
+ @service = AwsSdb::Service.new
102
+ @domain = "test-#{UUID.random_create.to_s}"
103
+ @service.list_domains[0].each do |d|
104
+ @service.delete_domain(d) if d =~ /^test/
105
+ end
106
+ @service.create_domain(@domain)
107
+ end
108
+
109
+ after do
110
+ @service.delete_domain(@domain)
111
+ end
112
+
113
+ it "should be able to delete an existing domain" do
114
+ lambda { @service.delete_domain(@domain) }.should_not raise_error
115
+ end
116
+
117
+ it "should not raise an error trying to delete a non-existing domain" do
118
+ lambda {
119
+ @service.delete_domain(UUID.random_create.to_s)
120
+ }.should_not raise_error
121
+ end
122
+ end
123
+
124
+ describe Service, "when managing items" do
125
+ before(:all) do
126
+ @service = AwsSdb::Service.new
127
+ @domain = "test-#{UUID.random_create.to_s}"
128
+ @service.list_domains[0].each do |d|
129
+ @service.delete_domain(d) if d =~ /^test/
130
+ end
131
+ @service.create_domain(@domain)
132
+ @item = "test-#{UUID.random_create.to_s}"
133
+ @attributes = {
134
+ :question => 'What is the answer?',
135
+ :answer => [ true, 'testing123', 4.2, 42, 420 ]
136
+ }
137
+ end
138
+
139
+ after(:all) do
140
+ @service.delete_domain(@domain)
141
+ end
142
+
143
+ it "should be able to put attributes" do
144
+ lambda {
145
+ @service.put_attributes(@domain, @item, @attributes)
146
+ }.should_not raise_error
147
+ end
148
+
149
+ it "should be able to get attributes" do
150
+ result = nil
151
+ lambda {
152
+ result = @service.get_attributes(@domain, @item)
153
+ }.should_not raise_error
154
+ result.should_not be_nil
155
+ result.should_not be_empty
156
+ result.has_key?('answer').should == true
157
+ @attributes[:answer].each do |v|
158
+ result['answer'].include?(v.to_s).should == true
159
+ end
160
+ end
161
+
162
+ it "should be able to query" do
163
+ result = nil
164
+ lambda {
165
+ result = @service.query(@domain, "[ 'answer' = '42' ]")[0]
166
+ }.should_not raise_error
167
+ result.should_not be_nil
168
+ result.should_not be_empty
169
+ result.should_not be_nil
170
+ result.include?(@item).should == true
171
+ end
172
+
173
+ it "should be able to delete attributes" do
174
+ lambda {
175
+ @service.delete_attributes(@domain, @item)
176
+ }.should_not raise_error
177
+ end
178
+ end
179
+
180
+ # TODO Pull the specs from the amazon docs and write more rspec
181
+ # 100 attributes per each call
182
+ # 256 total attribute name-value pairs per item
183
+ # 250 million attributes per domain
184
+ # 10 GB of total user data storage per domain
185
+ # ...etc...
@@ -0,0 +1,4 @@
1
+ $TESTING=true
2
+ $:.push File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'aws_sdb'
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: caring-aws-sdb
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: true
5
+ segments:
6
+ - 0
7
+ - 3
8
+ - 2
9
+ - caring
10
+ - 1
11
+ version: 0.3.2.caring.1
12
+ platform: ruby
13
+ authors:
14
+ - Tim Dysinger
15
+ - David Dollar
16
+ autorequire:
17
+ bindir: bin
18
+ cert_chain: []
19
+
20
+ date: 2009-10-02 00:00:00 -07:00
21
+ default_executable:
22
+ dependencies:
23
+ - !ruby/object:Gem::Dependency
24
+ name: uuidtools
25
+ prerelease: false
26
+ requirement: &id001 !ruby/object:Gem::Requirement
27
+ requirements:
28
+ - - ">="
29
+ - !ruby/object:Gem::Version
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rspec
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 0
44
+ version: "0"
45
+ type: :development
46
+ version_requirements: *id002
47
+ description: Amazon SDB API
48
+ email: ddollar@gmail.com
49
+ executables: []
50
+
51
+ extensions: []
52
+
53
+ extra_rdoc_files:
54
+ - LICENSE
55
+ - README
56
+ files:
57
+ - LICENSE
58
+ - README
59
+ - Rakefile
60
+ - VERSION
61
+ - aws-sdb.gemspec
62
+ - lib/aws_sdb.rb
63
+ - lib/aws_sdb/error.rb
64
+ - lib/aws_sdb/service.rb
65
+ - spec/aws_sdb/service_spec.rb
66
+ - spec/spec_helper.rb
67
+ has_rdoc: true
68
+ homepage: http://github.com/ddollar/aws-sdb
69
+ licenses: []
70
+
71
+ post_install_message:
72
+ rdoc_options:
73
+ - --charset=UTF-8
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ segments:
81
+ - 0
82
+ version: "0"
83
+ required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ segments:
88
+ - 0
89
+ version: "0"
90
+ requirements: []
91
+
92
+ rubyforge_project:
93
+ rubygems_version: 1.3.6
94
+ signing_key:
95
+ specification_version: 3
96
+ summary: Amazon SDB API
97
+ test_files:
98
+ - spec/aws_sdb/service_spec.rb
99
+ - spec/spec_helper.rb