tabster 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Piotr Murach
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.
@@ -0,0 +1,18 @@
1
+ # Tabster
2
+
3
+ Tabster is a Ruby generation library that ...
4
+
5
+ ## Contributing to tabster
6
+
7
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
8
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
9
+ * Fork the project
10
+ * Start a feature/bugfix branch
11
+ * Commit and push until you are happy with your contribution
12
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
13
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
14
+
15
+ ## Copyright
16
+
17
+ Copyright (c) 2011 Piotr Murach. See LICENSE.txt for
18
+ further details.
@@ -0,0 +1,45 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ begin
6
+ Bundler.setup(:default, :development)
7
+ rescue Bundler::BundlerError => e
8
+ $stderr.puts e.message
9
+ $stderr.puts "Run `bundle install` to install missing gems"
10
+ exit e.status_code
11
+ end
12
+ require 'rake'
13
+
14
+ require 'jeweler'
15
+ Jeweler::Tasks.new do |gem|
16
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
17
+ gem.name = "tabster"
18
+ gem.homepage = "http://github.com/peter-murach/tabster"
19
+ gem.license = "MIT"
20
+ gem.summary = %Q{TODO: one-line summary of your gem}
21
+ gem.description = %Q{TODO: longer description of your gem}
22
+ gem.email = "pmurach@gmail.com"
23
+ gem.authors = ["Piotr Murach"]
24
+ # dependencies defined in Gemfile
25
+ end
26
+ Jeweler::RubygemsDotOrgTasks.new
27
+
28
+ require 'rspec/core'
29
+ require 'rspec/core/rake_task'
30
+ RSpec::Core::RakeTask.new(:spec) do |spec|
31
+ spec.pattern = FileList['spec/**/*_spec.rb']
32
+ end
33
+
34
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
35
+ spec.pattern = 'spec/**/*_spec.rb'
36
+ spec.rcov = true
37
+ end
38
+
39
+ require 'cucumber/rake/task'
40
+ Cucumber::Rake::Task.new(:features)
41
+
42
+ task :default => :spec
43
+
44
+ require 'yard'
45
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,13 @@
1
+ require 'bundler'
2
+ begin
3
+ Bundler.setup(:default, :development)
4
+ rescue Bundler::BundlerError => e
5
+ $stderr.puts e.message
6
+ $stderr.puts "Run `bundle install` to install missing gems"
7
+ exit e.status_code
8
+ end
9
+
10
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
11
+ require 'tabster'
12
+
13
+ require 'rspec/expectations'
@@ -0,0 +1,9 @@
1
+ Feature: something something
2
+ In order to something something
3
+ A user something something
4
+ something something something
5
+
6
+ Scenario: something something
7
+ Given inspiration
8
+ When I create a sweet new gem
9
+ Then everyone should see how awesome I am
@@ -0,0 +1,21 @@
1
+ module Tabster
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("../../templates", __FILE__)
5
+
6
+ desc "Creates a Tabster initializer and copies css styles to your application"
7
+
8
+ def copy_initializer
9
+ template 'tabster.rb', 'config/initializers/tabster.rb'
10
+ end
11
+
12
+ def copy_stylesheet
13
+ copy_file "tabster.css", File.join('app/assets/stylesheets', "#{file_name}.css")
14
+ end
15
+
16
+ def show_readme
17
+ readme "README" if behavior == :invoke
18
+ end
19
+ end
20
+ end
21
+ end # Tabster
@@ -0,0 +1,22 @@
1
+ # Use this hook to specify the routes for all your tabs
2
+ Tabster.configure do |config|
3
+ # Configure the name used to identify tabs in your markup.
4
+ # This can be overwritten when invoking #tabs_for method
5
+ # in your views.
6
+ config.name = :admin
7
+
8
+ # Configure routes used for tab generation. Each tab entry
9
+ # should invoke #add method and pass following parameters
10
+ # :name and [:path_to].
11
+ # All paths are loaded in the order of their declartion.
12
+ config.draw do |map|
13
+ # map.add :dashboard, :path_to => '/admin/dashboard'
14
+
15
+ # If you want to add second level of tab navigation add
16
+ # extra block after the #add method call.
17
+
18
+ # map.add :posts, :path_to => '/admin/blog/posts' do |nested|
19
+ # nested.add :notes, :path_to => '/admin/blog/bla'
20
+ # end
21
+ end
22
+ end
@@ -0,0 +1,32 @@
1
+ require 'tabster/configurable'
2
+ require 'tabster/version'
3
+ require 'tabster/helpers'
4
+
5
+ module Tabster
6
+ include Tabster::Configurable
7
+ include Tabster::Version
8
+
9
+ SEPARATORS = %w( / )
10
+ REQUIRED_TAB_FRAGMENTS = [:path_to]
11
+ ALLOWED_PATH_FRAGMENTS = [:controller, :action]
12
+
13
+ class << self
14
+ delegate :config, :configure, :to => Tabster::Configurable
15
+
16
+ def initialize
17
+ raise "ActionController is not available yet." unless defined?(ActionController)
18
+ ActionController::Base.send(:include, self)
19
+ ActionController::Base.send(:helper, Tabster::Helpers)
20
+ end
21
+
22
+ def version
23
+ Tabster::Version::STRING
24
+ end
25
+ end
26
+ end
27
+
28
+ if defined?(Rails::Railtie)
29
+ require 'tabster/railtie'
30
+ else
31
+ Tabster.send(:initialize)
32
+ end
@@ -0,0 +1,71 @@
1
+ module Tabster
2
+ module Configurable
3
+
4
+ # Holds all the parameters for the Tabster initializer and defaults
5
+ # that should suite most needs. It's possible to overwrite those as well.
6
+ # This class is passed through the block running on the Tabster module.
7
+ class Configuration
8
+
9
+ attr_accessor_with_default :name, :main
10
+ attr_accessor_with_default :css_template, :main
11
+ attr_accessor_with_default :helpers, []
12
+ attr_accessor_with_default :includes, []
13
+ attr_accessor :tab_set
14
+
15
+ def initialize
16
+ initialize_tab_set!
17
+ end
18
+
19
+ def initialize_tab_set!
20
+ @tab_set = TabSet.new
21
+ end
22
+
23
+ # Reload the tab ordering
24
+ def reload
25
+ @tab_set = tab_set.nil? ? nil : tab_set
26
+ end
27
+
28
+ # add class methods to return tabnavs
29
+ #self.class_eval
30
+ # define_method
31
+ #end
32
+
33
+ def draw
34
+ yield @tab_set
35
+ @tab_set
36
+ end
37
+
38
+ def validate!
39
+ raise ConfigurationError.new("Tab navigation needs name as a String or Symbol \n e.i. config.name = :admin") unless self.name.is_a? String or self.name.is_a? Symbol
40
+ end
41
+
42
+ def configuration_path
43
+ "#{RAILS_ROOT}/config/initializers/tabacious.rb"
44
+ end
45
+
46
+ class ConfigurationError < StandardError; end
47
+
48
+ end # Configuration
49
+
50
+ class << self
51
+
52
+ attr_reader :config
53
+
54
+ def config
55
+ @@config ||= Configuration.new
56
+ end
57
+
58
+ def configure(&block)
59
+ raise "configure must be sent a block" unless block_given?
60
+ yield config
61
+ config.validate!
62
+ end
63
+
64
+ def to_hash
65
+ { :name => @@config.name, :css_template => @@config.css_template}
66
+ end
67
+
68
+ end
69
+
70
+ end # Configurable
71
+ end # Tabster
@@ -0,0 +1,59 @@
1
+ module Tabster
2
+ module Helpers
3
+ #include CssTemplate
4
+
5
+ # Use
6
+ # <%= tabs_for :name do %>
7
+ # ...
8
+ # <% end %>
9
+ # or just <%= tabacious :name %>
10
+ #
11
+ def tabs_for(name, options = {}, &block)
12
+ raise ArgumentError, "Missing name parameter in method call." unless name
13
+ #raise ArgumentError, "Missing block in method call." unless block_given?
14
+
15
+ #css_template_name = ::Utility::Config.configuration.css_template
16
+ #unless css_template_name
17
+ #concat render_css_template(css_template_name)
18
+ #end
19
+ puts "CONTROLLER: #{request.request_uri}"
20
+ tabnav_name = name.to_s.parameterize.wrapped_string + '_tabnav'
21
+ html = content_tag(:div, render_tabnav_tabs(name), :id => "#{tabnav_name}_wrapper")
22
+ puts "HTML RENDERING: #{html}"
23
+ if block_given?
24
+ html << content_tag(:div, capture(&block), :id => "#{tabnav_name}_content")
25
+ concat html
26
+ nil
27
+ end
28
+ html
29
+ end
30
+
31
+ private
32
+
33
+ def render_tabnav_tabs(tabnav_name)
34
+ puts "CONFIGURATION : #{::Tabster.config.inspect}"
35
+ tab_set = ::Tabster.config.tab_set
36
+ return if tab_set.empty?
37
+
38
+ list_items = []
39
+ tab_set.each do |name, tab|
40
+ li_options = {}
41
+ li_options[:id] = "#{name.to_s.parameterize.wrapped_string}_container"
42
+ #li_options[:class] = tab.highlighted?
43
+ if tab.path
44
+ active = request.request_uri =~ %r[#{tab.path}] ? 'active' : ''
45
+ list_items << content_tag(:li, link_to("#{name.to_s.camelize}", tab.path, :class => active), li_options )
46
+ else
47
+ list_items << content_tag(:li, content_tag(:span, "#{name.to_s.camelize}"))
48
+ end
49
+ end
50
+ content_tag(:ul, list_items, :id => "#{tabnav_name.to_s.parameterize.wrapped_string}_tabnav")
51
+ end
52
+
53
+ def config_cache
54
+ return @config_cache if @confige_cache
55
+ @config_cache = ::Utility::Config.configuration
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,9 @@
1
+ if defined?(Rails::Railtie)
2
+ module Tabster
3
+ class Railtie < Rails::Railtie
4
+ initializer :tabacious do
5
+ Tabster.initialize
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,38 @@
1
+ module Tabster
2
+ class Tab
3
+ attr_accessor :path
4
+ attr_accessor :name
5
+ attr_accessor :tabs
6
+
7
+ attr_accessor_with_default :priority, 1
8
+ attr_accessor_with_default :title, 'default'
9
+ attr_accessor_with_default :sub_tabnav, []
10
+
11
+ def initialize(options = {}, &block)
12
+ puts " SEGMENTS IN TAB: #{options.inspect}"
13
+
14
+ if block_given?
15
+ @tabs = TabSet.new
16
+ puts "BLOCK: #{block.inspect}"
17
+ yield @tabs
18
+ end
19
+
20
+ @path = options[:path_to] || '/'
21
+ @highlight = options[:highlights_on] || ''
22
+ @priority = options[:priority] || 1
23
+ end
24
+
25
+ def add_tab(name, options, &block)
26
+ Tabnav.new.add_tab(name, options, &block)
27
+ end
28
+
29
+ def extract_tabs(collection = [])
30
+ return nil unless collection.is_a? Array
31
+ collection.map { |item| self.new( { :title => item.title, :path => item.path }) }
32
+ end
33
+
34
+ def freeze
35
+ self
36
+ end
37
+ end #Tab
38
+ end #Tabster
@@ -0,0 +1,65 @@
1
+ #TODO make use of the passed block to create subitems in the navigation.
2
+
3
+ module Tabster
4
+ class TabBuilder
5
+
6
+ # Construct and return a tab with the given path and options.
7
+ def build(options, &block)
8
+ segments = extract_tab_segments(options)
9
+ puts "EXTRACTED FRAGMENTS: #{segments.inspect}"
10
+ tab = Tab.new(segments, &block)
11
+ tab.freeze
12
+ end
13
+
14
+ def extract_tab_segments(options)
15
+ segments = {}
16
+ options.each do |key, value|
17
+ if [:path_to].include?(key)
18
+ segments[key] = extract_path_segment(value)
19
+ elsif [:priority].include?(key)
20
+ segments[key] = value.to_i
21
+ elsif [:highlights_on].include?(key)
22
+ if value.is_a? Proc
23
+ segments[key] = value
24
+ end
25
+ end
26
+ end
27
+ segments
28
+ end
29
+
30
+ def extract_path_segment(path_to)
31
+ case path_to.class.to_s
32
+ when 'String'
33
+ normalize_path(wrap_path_with_slashes(path_to.to_s))
34
+ when 'Symbol'
35
+ normalize_path(wrap_path_with_slashes(path_to.to_s))
36
+ when 'Hash'
37
+ # if controller key given, make action an explicit param
38
+ action = path_to[:action].nil? ? 'index' : path_to[:action]
39
+ if path_to[:controller]
40
+ normalize_path("#{path_to[:controller]}/#{action}")
41
+ else
42
+ raise ArgumentError, ":path_to is invalid in tab parameters. Need to specify at least the controller, i.e. :controller => :admin"
43
+ end
44
+ end
45
+ end
46
+
47
+ def wrap_path_with_slashes(path)
48
+ path = "/#{path}" unless path[0] == ?/
49
+ path = "#{path}/" unless path[-1] == ?/
50
+ end
51
+
52
+ # Returns a path cleaned of double-shlashes and relative path references.
53
+ def normalize_path(path)
54
+ path = path.
55
+ gsub("//", "/").
56
+ gsub("\\\\", "\\").
57
+ gsub(%r{(.)[\\/]$}, '\1')
58
+
59
+ re = %r{[^/\\]+[/\\]\.\.[/\\]}
60
+ path.gsub!(re, "") while path.match(re)
61
+ path
62
+ end
63
+
64
+ end # TabBuilder
65
+ end # Tabster
@@ -0,0 +1,96 @@
1
+ require 'active_support'
2
+
3
+ module Tabster
4
+ class TabSet
5
+ include Enumerable
6
+ include ActiveSupport
7
+
8
+ attr_reader :tabs
9
+ attr_reader :name
10
+
11
+ def initialize
12
+ clear!
13
+ end
14
+
15
+ def add(name, options = {}, &block)
16
+ add_tab(name, options, &block)
17
+ end
18
+
19
+ def add_tab(name, options = {}, &block)
20
+ puts "ADDING TAB: #{options.inspect}"
21
+ raise_tab_name_error if name.nil?
22
+ options = options.dup.symbolize_keys
23
+ options.assert_valid_keys(:path_to, :highlights_on, :priority)
24
+ ensure_required_fragments(options)
25
+ tab = builder.build(options, &block)
26
+ @tabs[name.to_sym] = tab
27
+ tab
28
+ end
29
+
30
+ def ensure_required_fragments(options)
31
+ Tabster::REQUIRED_TAB_FRAGMENTS.each do |fragment|
32
+ unless options.has_key? fragment.to_sym
33
+ raise ArgumentError, "Tab fragment :path_to cannot be optional."
34
+ end
35
+ end
36
+ end
37
+
38
+ def builder
39
+ @builder ||= TabBuilder.new
40
+ end
41
+
42
+ def method_missing(tab_name, *args, &proc)
43
+ super
44
+ end
45
+
46
+ def get(name)
47
+ @tabs[name.to_sym]
48
+ end
49
+
50
+ def clear!
51
+ @tabs = ActiveSupport::OrderedHash.new
52
+ end
53
+
54
+ def tab_names
55
+ @tabs.keys
56
+ end
57
+
58
+ def each
59
+ tabs.each { |name, params| yield name, params }
60
+ self
61
+ end
62
+
63
+ def names
64
+ tabs.keys
65
+ end
66
+
67
+ def sort!(sort_key = :priority, &block)
68
+ sort_key = sort_key.to_sym
69
+ if block_given?
70
+ tabs.sort &block
71
+ else
72
+ tabs.sort { |tab1, tab2| tab1[1][sort_key] <=> tab2[1][sort_key] }
73
+ end
74
+ end
75
+
76
+ def empty?
77
+ tabs.empty?
78
+ end
79
+
80
+ def length
81
+ tabs.length
82
+ end
83
+
84
+ def raise_tab_name_error
85
+ raise TabRoutingError, "Failed to generate tab from params."
86
+ end
87
+
88
+ class TabRoutingError < StandardError; end
89
+ class TabWrongParams < StandardError; end
90
+
91
+ alias []= add_tab
92
+ alias [] get
93
+ #alias clear clear!
94
+
95
+ end # TabSet
96
+ end # Tabster
@@ -0,0 +1,9 @@
1
+ module Tabster
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ TINY = 0
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end # Version
9
+ end # Tabster
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'tabster'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Tabster" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tabster
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Piotr Murach
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-11-20 00:00:00 +00:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ~>
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 2
29
+ - 4
30
+ - 0
31
+ version: 2.4.0
32
+ type: :development
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: cucumber
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ segments:
42
+ - 0
43
+ version: "0"
44
+ type: :development
45
+ version_requirements: *id002
46
+ - !ruby/object:Gem::Dependency
47
+ name: bundler
48
+ prerelease: false
49
+ requirement: &id003 !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ segments:
54
+ - 1
55
+ - 0
56
+ - 0
57
+ version: 1.0.0
58
+ type: :development
59
+ version_requirements: *id003
60
+ - !ruby/object:Gem::Dependency
61
+ name: jeweler
62
+ prerelease: false
63
+ requirement: &id004 !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ~>
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 1
69
+ - 6
70
+ - 4
71
+ version: 1.6.4
72
+ type: :development
73
+ version_requirements: *id004
74
+ - !ruby/object:Gem::Dependency
75
+ name: simplecov
76
+ prerelease: false
77
+ requirement: &id005 !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ~>
80
+ - !ruby/object:Gem::Version
81
+ segments:
82
+ - 0
83
+ - 4
84
+ version: "0.4"
85
+ type: :development
86
+ version_requirements: *id005
87
+ - !ruby/object:Gem::Dependency
88
+ name: guard-rspec
89
+ prerelease: false
90
+ requirement: &id006 !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ segments:
95
+ - 0
96
+ version: "0"
97
+ type: :development
98
+ version_requirements: *id006
99
+ description: " "
100
+ email: ""
101
+ executables: []
102
+
103
+ extensions: []
104
+
105
+ extra_rdoc_files: []
106
+
107
+ files:
108
+ - Rakefile
109
+ - features/step_definitions/tabster_steps.rb
110
+ - features/support/env.rb
111
+ - features/tabster.feature
112
+ - lib/generators/tabster/install_generator.rb
113
+ - lib/generators/templates/tabster.rb
114
+ - lib/tabster/configurable.rb
115
+ - lib/tabster/helpers.rb
116
+ - lib/tabster/railtie.rb
117
+ - lib/tabster/tab.rb
118
+ - lib/tabster/tab_builder.rb
119
+ - lib/tabster/tab_set.rb
120
+ - lib/tabster/version.rb
121
+ - lib/tabster.rb
122
+ - spec/spec_helper.rb
123
+ - spec/tabster_spec.rb
124
+ - README.md
125
+ - LICENSE.txt
126
+ has_rdoc: true
127
+ homepage: https://github.com/peter-murach/tabster
128
+ licenses: []
129
+
130
+ post_install_message:
131
+ rdoc_options: []
132
+
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ segments:
140
+ - 0
141
+ version: "0"
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ segments:
147
+ - 0
148
+ version: "0"
149
+ requirements: []
150
+
151
+ rubyforge_project:
152
+ rubygems_version: 1.3.6
153
+ signing_key:
154
+ specification_version: 3
155
+ summary: Rails tabs generation library
156
+ test_files: []
157
+