tabletastic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Joshua Davey
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,51 @@
1
+ = Tabletastic
2
+
3
+ Inspired by the projects table_builder and formtastic,
4
+ I realized how often I created tables for my active record collections.
5
+ This is my attempt to simply this (the default scaffold):
6
+
7
+ <table>
8
+ <tr>
9
+ <th>Title</th>
10
+ <th>Body</th>
11
+ <th>Author Id</th>
12
+ </tr>
13
+ <% for post in @posts %>
14
+ <tr>
15
+ <td><%=h post.title %></td>
16
+ <td><%=h post.body %></td>
17
+ <td><%=h post.author_id %></td>
18
+ <td><%=h author.name %></td>
19
+ <td><%= link_to "Show", post %></td>
20
+ <td><%= link_to "Edit", edit_post_path(post) %></td>
21
+ <td><%= link_to "Destroy", post, :confirm => 'Are you sure?', :method => :delete %></td>
22
+ </tr>
23
+ <% end %>
24
+ </table>
25
+
26
+ into this:
27
+
28
+ <% table_for(@posts) do |t| %>
29
+ <%= t.data :title, :body, :author %>
30
+ <% end %>
31
+
32
+
33
+ == Warning
34
+ THIS PROJECT IS UNDER HEAVY DEVELOPMENT.
35
+ IT IS NOT RECOMMENDED FOR USE IN PRODUCTION APPLICATIONS
36
+
37
+
38
+ == Note on Patches/Pull Requests
39
+
40
+ * Fork the project.
41
+ * Make your feature addition or bug fix.
42
+ * Add tests for it. This is important so I don't break it in a
43
+ future version unintentionally.
44
+ * Commit, do not mess with rakefile, version, or history.
45
+ (if you want to have your own version, that is fine but
46
+ bump version in a commit by itself I can ignore when I pull)
47
+ * Send me a pull request. Bonus points for topic branches.
48
+
49
+ == Copyright
50
+
51
+ Copyright (c) 2009 Joshua Davey. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,59 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ GEM = "tabletastic"
5
+ LONGDESCRIPTION = %Q{A table builder for active record collections \
6
+ that produces semantically rich and accessible markup}
7
+
8
+ begin
9
+ require 'jeweler'
10
+ Jeweler::Tasks.new do |s|
11
+ s.name = GEM
12
+ s.summary = %Q{A smarter table builder for Rails collections}
13
+ s.description = LONGDESCRIPTION
14
+ s.email = "josh@joshuadavey.com"
15
+ s.homepage = "http://github.com/jgdavey/tabletastic"
16
+ s.authors = ["Joshua Davey"]
17
+ s.require_path = 'lib'
18
+
19
+ s.add_development_dependency "rspec", ">= 1.2.9"
20
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
21
+ end
22
+ Jeweler::GemcutterTasks.new
23
+ rescue LoadError
24
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
25
+ end
26
+
27
+ require 'spec/rake/spectask'
28
+ Spec::Rake::SpecTask.new(:spec) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.spec_files = FileList['spec/**/*_spec.rb']
31
+ end
32
+
33
+ desc 'Test the tabletastic plugin with specdoc formatting and colors'
34
+ Spec::Rake::SpecTask.new('specdoc') do |t|
35
+ t.spec_files = FileList['spec/**/*_spec.rb']
36
+ t.spec_opts = ["--format specdoc", "-c"]
37
+ end
38
+
39
+ desc 'Test the tabletastic plugin with rcov'
40
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
41
+ spec.libs << 'lib' << 'spec'
42
+ spec.pattern = 'spec/**/*_spec.rb'
43
+ spec.rcov = true
44
+ spec.rcov_opts = ['--exclude', 'spec,Library']
45
+ end
46
+
47
+ task :spec => :check_dependencies
48
+
49
+ task :default => :spec
50
+
51
+ require 'rake/rdoctask'
52
+ Rake::RDocTask.new do |rdoc|
53
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
54
+
55
+ rdoc.rdoc_dir = 'rdoc'
56
+ rdoc.title = "Tabletastic #{version}"
57
+ rdoc.rdoc_files.include('README*')
58
+ rdoc.rdoc_files.include('lib/**/*.rb')
59
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,109 @@
1
+ module Tabletastic
2
+
3
+ def table_for(collection, *args)
4
+ options = args.extract_options!
5
+ options[:html] ||= {}
6
+ options[:html][:id] ||= get_id_for(collection)
7
+ concat(tag(:table, options[:html], true))
8
+ yield TableBuilder.new(collection, self)
9
+ concat("</table>")
10
+ end
11
+
12
+ def get_id_for(collection)
13
+ !collection.empty? && collection.first.class.to_s.tableize
14
+ end
15
+
16
+ class TableBuilder
17
+ @@association_methods = %w[to_label display_name full_name name title username login value to_s]
18
+
19
+ def initialize(collection, template)
20
+ @collection, @template = collection, template
21
+ end
22
+
23
+ def data(*args, &block)
24
+ if block_given?
25
+ yield self
26
+ @template.concat(headers)
27
+ @template.concat(body)
28
+ else
29
+ @fields = args unless args.empty?
30
+ headers + body
31
+ end
32
+ end
33
+
34
+ def headers
35
+ content_tag(:thead) do
36
+ header_row
37
+ end
38
+ end
39
+
40
+ def header_row
41
+ output = "<tr>"
42
+ fields.each do |field|
43
+ output += content_tag(:th, field.to_s.humanize)
44
+ end
45
+ output += "</tr>"
46
+ end
47
+
48
+ def body
49
+ content_tag(:tbody) do
50
+ body_rows
51
+ end
52
+ end
53
+
54
+ def body_rows
55
+ @collection.inject("") do |rows, record|
56
+ rowclass = @template.cycle("odd","even")
57
+ rows += @template.content_tag_for(:tr, record, :class => rowclass) do
58
+ tds_for_row(record)
59
+ end
60
+ end
61
+ end
62
+
63
+ def tds_for_row(record)
64
+ fields.inject("") do |cells, field|
65
+ cells += content_tag(:td, cell_for(record, field))
66
+ end
67
+ end
68
+
69
+ def cell_for(record, method_or_attribute)
70
+ result = record.send(method_or_attribute)
71
+ return result if result.is_a?(String)
72
+ to_string = detect_string_method(result)
73
+ result.send(to_string) if to_string
74
+ end
75
+
76
+ def detect_string_method(association)
77
+ @@association_methods.detect { |method| association.respond_to?(method) }
78
+ end
79
+
80
+ def cell(method_or_attribute)
81
+ @fields ||= []
82
+ @fields << method_or_attribute.to_sym
83
+ return ""
84
+ end
85
+
86
+ def fields
87
+ return @fields if defined?(@fields)
88
+ if @collection.empty?
89
+ @fields = []
90
+ else
91
+ object = @collection.first
92
+ associations = object.class.reflect_on_all_associations(:belongs_to) if object.class.respond_to?(:reflect_on_all_associations)
93
+ @fields = object.class.content_columns.map(&:name)
94
+ if associations
95
+ associations = associations.map(&:name)
96
+ @fields += associations
97
+ end
98
+ @fields -= %w[created_at updated_at created_on updated_on lock_version version]
99
+ @fields.map!(&:to_sym)
100
+ end
101
+ end
102
+
103
+ private
104
+
105
+ def content_tag(name, content = nil, options = nil, escape = true, &block)
106
+ @template.content_tag(name, content, options, escape, &block)
107
+ end
108
+ end
109
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,3 @@
1
+ # Include hook code here
2
+ require File.join(File.dirname(__FILE__), *%w[.. lib tabletastic])
3
+ ActionView::Base.send :include, Tabletastic
data/spec/spec.opts ADDED
@@ -0,0 +1,2 @@
1
+ --diff
2
+ --color
@@ -0,0 +1,105 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ require 'rubygems'
3
+
4
+ def smart_require(lib_name, gem_name, gem_version = '>= 0.0.0')
5
+ begin
6
+ require lib_name if lib_name
7
+ rescue LoadError
8
+ if gem_name
9
+ gem gem_name, gem_version
10
+ require lib_name if lib_name
11
+ end
12
+ end
13
+ end
14
+
15
+ smart_require 'spec', 'spec', '>= 1.2.8'
16
+ require 'spec/autorun'
17
+ smart_require false, 'rspec-rails', '>= 1.2.7.1'
18
+ smart_require 'hpricot', 'hpricot', '>= 0.6.1'
19
+ smart_require 'rspec_hpricot_matchers', 'rspec_hpricot_matchers', '>= 1.0.0'
20
+ smart_require 'active_support', 'activesupport', '>= 2.3.4'
21
+ smart_require 'action_controller', 'actionpack', '>= 2.3.4'
22
+ smart_require 'action_view', 'actionpack', '>= 2.3.4'
23
+
24
+ Spec::Runner.configure do |config|
25
+ config.include(RspecHpricotMatchers)
26
+ end
27
+
28
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
29
+ require 'tabletastic'
30
+
31
+ module TabletasticSpecHelper
32
+ include ActionView::Helpers::UrlHelper
33
+ include ActionView::Helpers::TagHelper
34
+ include ActionView::Helpers::TextHelper
35
+ include ActionView::Helpers::ActiveRecordHelper
36
+ include ActionView::Helpers::RecordIdentificationHelper
37
+ include ActionView::Helpers::RecordTagHelper
38
+ include ActionView::Helpers::CaptureHelper
39
+ include ActiveSupport
40
+ include ActionController::PolymorphicRoutes
41
+
42
+ def self.included(base)
43
+ base.class_eval do
44
+ attr_accessor :output_buffer
45
+ def protect_against_forgery?
46
+ false
47
+ end
48
+ end
49
+ end
50
+
51
+ module ::RspecHpricotMatchers
52
+ def have_table_with_tag(selector, inner_text_or_options = nil, options = {}, &block)
53
+ HaveTag.new("table", nil, {}) &&
54
+ HaveTag.new(selector, inner_text_or_options, options, &block)
55
+ end
56
+ end
57
+
58
+ class ::Post
59
+ def id
60
+ end
61
+ end
62
+ class ::Author
63
+ end
64
+
65
+ def mock_everything
66
+ def post_path(post); "/posts/#{post.id}"; end
67
+ def edit_post_path(post); "/posts/#{post.id}/edit"; end
68
+
69
+ # Sometimes we need a mock @post object and some Authors for belongs_to
70
+ @post = mock('post')
71
+ @post.stub!(:class).and_return(::Post)
72
+ @post.stub!(:id).and_return(nil)
73
+ @post.stub!(:author)
74
+ ::Post.stub!(:human_attribute_name).and_return { |column_name| column_name.humanize }
75
+ ::Post.stub!(:human_name).and_return('Post')
76
+
77
+ @fred = mock('user')
78
+ @fred.stub!(:class).and_return(::Author)
79
+ @fred.stub!(:name).and_return('Fred Smith')
80
+ @fred.stub!(:id).and_return(37)
81
+
82
+ ::Author.stub!(:find).and_return([@fred])
83
+ ::Author.stub!(:human_attribute_name).and_return { |column_name| column_name.humanize }
84
+ ::Author.stub!(:human_name).and_return('Author')
85
+ ::Author.stub!(:reflect_on_association).and_return { |column_name| mock('reflection', :options => {}, :klass => Post, :macro => :has_many) if column_name == :posts }
86
+
87
+ @freds_post = mock('post')
88
+ @freds_post.stub!(:class).and_return(::Post)
89
+ @freds_post.stub!(:title).and_return('Fred\'s Post')
90
+ @freds_post.stub!(:body)
91
+ @freds_post.stub!(:id).and_return(19)
92
+ @freds_post.stub!(:author).and_return(@fred)
93
+ @freds_post.stub!(:author_id).and_return(@fred.id)
94
+ @fred.stub!(:posts).and_return([@freds_post])
95
+ @fred.stub!(:post_ids).and_return([@freds_post.id])
96
+
97
+ @mock_reflection_belongs_to_author = mock('reflection', :options => {}, :name => :author, :klass => ::Author, :macro => :belongs_to)
98
+
99
+ ::Post.stub!(:reflect_on_association).and_return do |column_name|
100
+ @mock_reflection_belongs_to_author if column_name == :author
101
+ end
102
+
103
+ ::Post.stub!(:reflect_on_all_associations).with(:belongs_to).and_return([])
104
+ end
105
+ end
@@ -0,0 +1,206 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ include TabletasticSpecHelper
3
+ include Tabletastic
4
+
5
+
6
+ describe "Tabletastic#table_for" do
7
+
8
+ before do
9
+ @output_buffer = ''
10
+ end
11
+
12
+ describe "basics" do
13
+ it "should start with an empty output buffer" do
14
+ output_buffer.should_not have_tag("table")
15
+ end
16
+
17
+ it "should build a basic table" do
18
+ table_for([]) do |t|
19
+ end
20
+ output_buffer.should have_tag("table")
21
+ end
22
+
23
+ context "headers and table body" do
24
+ before do
25
+ table_for([]) do |t|
26
+ concat(t.headers)
27
+ concat(t.body)
28
+ end
29
+ end
30
+
31
+ it "should build a basic table and headers" do
32
+ output_buffer.should have_table_with_tag("thead")
33
+ end
34
+
35
+ it "should build a basic table and body" do
36
+ output_buffer.should have_table_with_tag("tbody")
37
+ end
38
+ end
39
+
40
+ context "with options" do
41
+ it "should pass along html options" do
42
+ table_for([], :html => {:class => 'special'}) do |t|
43
+ end
44
+ output_buffer.should have_tag("table.special")
45
+ end
46
+ end
47
+ end
48
+
49
+ describe "#data" do
50
+ before do
51
+ mock_everything
52
+ ::Post.stub!(:content_columns).and_return([mock('column', :name => 'title'), mock('column', :name => 'body'), mock('column', :name => 'created_at')])
53
+ @post.stub!(:title).and_return("The title of the post")
54
+ @post.stub!(:body).and_return("Lorem ipsum")
55
+ @post.stub!(:created_at).and_return(Time.now)
56
+ @post.stub!(:id).and_return(2)
57
+ @posts = [@post]
58
+ end
59
+
60
+ context "without a block" do
61
+ context "with no other arguments" do
62
+ before do
63
+ table_for(@posts) do |t|
64
+ concat(t.data)
65
+ end
66
+ end
67
+
68
+ it "should output table with id of the class of the collection" do
69
+ output_buffer.should have_tag("table#posts")
70
+ end
71
+
72
+ it "should output headers" do
73
+ output_buffer.should have_table_with_tag("thead")
74
+ end
75
+
76
+ it "should have a <th> for each attribute" do
77
+ # title and body
78
+ output_buffer.should have_table_with_tag("th", :count => 2)
79
+ end
80
+
81
+ it "should include header for Title" do
82
+ output_buffer.should have_table_with_tag("th", "Title")
83
+ end
84
+
85
+ it "should include header for Body" do
86
+ output_buffer.should have_table_with_tag("th", "Body")
87
+ end
88
+
89
+ it "should output body" do
90
+ output_buffer.should have_table_with_tag("tbody")
91
+ end
92
+
93
+ it "should include a row for each record" do
94
+ output_buffer.should have_table_with_tag("tbody") do |tbody|
95
+ tbody.should have_tag("tr", :count => 1)
96
+ end
97
+ end
98
+
99
+ it "should have data for each field" do
100
+ output_buffer.should have_table_with_tag("td", "The title of the post")
101
+ output_buffer.should have_table_with_tag("td", "Lorem ipsum")
102
+ end
103
+
104
+ it "should include the id for the <tr> for each record" do
105
+ output_buffer.should have_table_with_tag("tr#post_#{@post.id}")
106
+ end
107
+
108
+ it "should cycle row classes" do
109
+ @output_buffer = ""
110
+ @posts = [@post, @post]
111
+ table_for(@posts) do |t|
112
+ concat(t.data)
113
+ end
114
+ output_buffer.should have_table_with_tag("tr.odd")
115
+ output_buffer.should have_table_with_tag("tr.even")
116
+ end
117
+
118
+ context "when collection has associations" do
119
+ it "should handle belongs_to associations" do
120
+ ::Post.stub!(:reflect_on_all_associations).with(:belongs_to).and_return([@mock_reflection_belongs_to_author])
121
+ @posts = [@freds_post]
122
+ @output_buffer = ""
123
+ table_for(@posts) do |t|
124
+ concat(t.data)
125
+ end
126
+ output_buffer.should have_table_with_tag("th", "Author")
127
+ output_buffer.should have_table_with_tag("td", "Fred Smith")
128
+ end
129
+ end
130
+ end
131
+
132
+ context "with a list of attributes" do
133
+ before do
134
+ table_for(@posts) do |t|
135
+ concat(t.data(:title, :created_at))
136
+ end
137
+ end
138
+
139
+ it "should call each method passed in, and only those methods" do
140
+ output_buffer.should have_table_with_tag("th", "Title")
141
+ output_buffer.should have_table_with_tag("th", "Created at")
142
+ output_buffer.should_not have_table_with_tag("th", "Body")
143
+ end
144
+ end
145
+ end
146
+
147
+ context "with a block" do
148
+ context "and normal columns" do
149
+ before do
150
+ table_for(@posts) do |t|
151
+ t.data do
152
+ concat(t.cell(:title))
153
+ concat(t.cell(:body))
154
+ end
155
+ end
156
+ end
157
+
158
+ it "should include the data for the fields passed in" do
159
+ output_buffer.should have_table_with_tag("th", "Title")
160
+ output_buffer.should have_tag("td", "The title of the post")
161
+ output_buffer.should have_tag("td", "Lorem ipsum")
162
+ end
163
+ end
164
+
165
+ context "and normal/association columns" do
166
+ before do
167
+ ::Post.stub!(:reflect_on_all_associations).with(:belongs_to).and_return([@mock_reflection_belongs_to_author])
168
+ @posts = [@freds_post]
169
+ table_for(@posts) do |t|
170
+ t.data do
171
+ concat(t.cell(:title))
172
+ concat(t.cell(:author))
173
+ end
174
+ end
175
+ end
176
+
177
+ it "should include normal columns" do
178
+ output_buffer.should have_table_with_tag("th:nth-child(1)", "Title")
179
+ output_buffer.should have_table_with_tag("td:nth-child(1)", "Fred's Post")
180
+ end
181
+
182
+ it "should include belongs_to associations" do
183
+ output_buffer.should have_table_with_tag("th:nth-child(2)", "Author")
184
+ output_buffer.should have_table_with_tag("td:nth-child(2)", "Fred Smith")
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ describe TableBuilder do
192
+ before do
193
+ mock_everything
194
+ ::Post.stub!(:content_columns).and_return([mock('column', :name => 'title'), mock('column', :name => 'body'), mock('column', :name => 'created_at')])
195
+ @posts = [@post, Post.new]
196
+ @builder = TableBuilder.new(@posts, nil)
197
+ end
198
+
199
+ it "should detect attributes" do
200
+ @builder.fields.should include(:title)
201
+ end
202
+
203
+ it "should reject marked attributes" do
204
+ @builder.fields.should_not include(:created_at)
205
+ end
206
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tabletastic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Joshua Davey
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-15 00:00:00 -06:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.2.9
24
+ version:
25
+ description: A table builder for active record collections that produces semantically rich and accessible markup
26
+ email: josh@joshuadavey.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README.rdoc
34
+ files:
35
+ - .document
36
+ - .gitignore
37
+ - LICENSE
38
+ - README.rdoc
39
+ - Rakefile
40
+ - VERSION
41
+ - lib/tabletastic.rb
42
+ - rails/init.rb
43
+ - spec/spec.opts
44
+ - spec/spec_helper.rb
45
+ - spec/tabletastic_spec.rb
46
+ has_rdoc: true
47
+ homepage: http://github.com/jgdavey/tabletastic
48
+ licenses: []
49
+
50
+ post_install_message:
51
+ rdoc_options:
52
+ - --charset=UTF-8
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: "0"
60
+ version:
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ requirements: []
68
+
69
+ rubyforge_project:
70
+ rubygems_version: 1.3.5
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: A smarter table builder for Rails collections
74
+ test_files:
75
+ - spec/spec_helper.rb
76
+ - spec/tabletastic_spec.rb