hoboken 0.0.1 → 0.9.0
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.
- checksums.yaml +5 -5
- data/.github/dependabot.yml +7 -0
- data/.github/workflows/ruby.yml +28 -0
- data/.rubocop.yml +33 -0
- data/.ruby-version +1 -0
- data/Gemfile +2 -0
- data/IDEAS.md +57 -0
- data/LICENSE.txt +1 -1
- data/README.md +21 -10
- data/Rakefile +14 -6
- data/bin/hoboken +2 -1
- data/hoboken.gemspec +53 -16
- data/lib/hoboken.rb +110 -23
- data/lib/hoboken/actions.rb +10 -6
- data/lib/hoboken/add_ons/github_action.rb +14 -0
- data/lib/hoboken/add_ons/heroku.rb +8 -23
- data/lib/hoboken/add_ons/internationalization.rb +10 -6
- data/lib/hoboken/add_ons/metrics.rb +39 -14
- data/lib/hoboken/add_ons/omniauth.rb +114 -47
- data/lib/hoboken/add_ons/rubocop.rb +40 -0
- data/lib/hoboken/add_ons/sequel.rb +76 -47
- data/lib/hoboken/add_ons/sprockets.rb +67 -65
- data/lib/hoboken/add_ons/travis.rb +6 -2
- data/lib/hoboken/add_ons/twbs.rb +80 -0
- data/lib/hoboken/generate.rb +112 -38
- data/lib/hoboken/templates/Gemfile.erb.tt +33 -10
- data/lib/hoboken/templates/README.md.tt +105 -35
- data/lib/hoboken/templates/Rakefile.tt +10 -22
- data/lib/hoboken/templates/classic.rb.tt +35 -8
- data/lib/hoboken/templates/config.ru.tt +5 -3
- data/lib/hoboken/templates/console.sh +5 -0
- data/lib/hoboken/templates/db.rb.tt +24 -0
- data/lib/hoboken/templates/github_action.tt +28 -0
- data/lib/hoboken/templates/gitignore +8 -1
- data/lib/hoboken/templates/metrics.rake.tt +10 -9
- data/lib/hoboken/templates/modular.rb.tt +40 -11
- data/lib/hoboken/templates/puma.rb.tt +21 -0
- data/lib/hoboken/templates/rspec.rake.tt +5 -0
- data/lib/hoboken/templates/rubocop.yml.tt +31 -0
- data/lib/hoboken/templates/sequel.rake +6 -4
- data/lib/hoboken/templates/server.sh +12 -0
- data/lib/hoboken/templates/setup.sh +7 -0
- data/lib/hoboken/templates/spec/app_spec.rb.tt +15 -0
- data/lib/hoboken/templates/spec/rack_matchers.rb.tt +56 -0
- data/lib/hoboken/templates/spec/spec_helper.rb.tt +41 -0
- data/lib/hoboken/templates/sprockets.rake +13 -7
- data/lib/hoboken/templates/sprockets_chain.rb +7 -3
- data/lib/hoboken/templates/sprockets_helper.rb +14 -10
- data/lib/hoboken/templates/support/rack_helpers.rb.tt +55 -0
- data/lib/hoboken/templates/support/rack_test_assertions.rb.tt +111 -0
- data/lib/hoboken/templates/test/test_helper.rb.tt +38 -27
- data/lib/hoboken/templates/test/unit/app_test.rb.tt +11 -3
- data/lib/hoboken/templates/test_unit.rake.tt +18 -0
- data/lib/hoboken/templates/views/index.erb.tt +10 -3
- data/lib/hoboken/templates/views/layout.erb.tt +4 -1
- data/lib/hoboken/version.rb +3 -1
- data/test/fixtures/Gemfile +3 -3
- data/test/fixtures/Gemfile.pristine +3 -2
- data/test/integration/add_on_test.rb +399 -136
- data/test/integration/generate_test.rb +170 -38
- data/test/test_helper.rb +54 -23
- data/test/unit/hoboken_actions_test.rb +70 -61
- metadata +441 -16
- data/.travis.yml +0 -5
- data/lib/hoboken/templates/test/support/rack_test_assertions.rb.tt +0 -92
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
workers_count = Integer(ENV['WEB_CONCURRENCY'] || 1)
|
4
|
+
threads_count = Integer(ENV['MAX_THREADS'] || 5)
|
5
|
+
threads threads_count, threads_count
|
6
|
+
|
7
|
+
port ENV['PORT'] || 9292
|
8
|
+
environment ENV['RACK_ENV'] || 'production'
|
9
|
+
|
10
|
+
if workers_count > 1
|
11
|
+
preload_app!
|
12
|
+
workers workers_count
|
13
|
+
|
14
|
+
# rubocop:disable Lint/EmptyBlock
|
15
|
+
before_fork do
|
16
|
+
end
|
17
|
+
|
18
|
+
on_worker_boot do
|
19
|
+
end
|
20
|
+
# rubocop:enable Lint/EmptyBlock
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require:
|
2
|
+
- rubocop-rake
|
3
|
+
<% if rspec? -%>
|
4
|
+
- rubocop-rspec
|
5
|
+
<% end -%>
|
6
|
+
|
7
|
+
AllCops:
|
8
|
+
Exclude:
|
9
|
+
- 'bin/*'
|
10
|
+
- 'tmp/*'
|
11
|
+
- 'vendor/bundle/**/*'
|
12
|
+
NewCops: enable
|
13
|
+
TargetRubyVersion: <%= RUBY_VERSION %>
|
14
|
+
|
15
|
+
Layout/SpaceAroundEqualsInParameterDefault:
|
16
|
+
EnforcedStyle: no_space
|
17
|
+
|
18
|
+
Metrics/ClassLength:
|
19
|
+
Max: 150
|
20
|
+
|
21
|
+
Metrics/MethodLength:
|
22
|
+
Max: 15
|
23
|
+
|
24
|
+
Layout/LineLength:
|
25
|
+
Max: 90
|
26
|
+
|
27
|
+
Style/BlockDelimiters:
|
28
|
+
EnforcedStyle: braces_for_chaining
|
29
|
+
|
30
|
+
Style/YodaCondition:
|
31
|
+
EnforcedStyle: require_for_equality_operators_only
|
@@ -1,19 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
namespace :db do
|
2
4
|
require 'sequel'
|
3
5
|
|
4
6
|
Sequel.extension :migration
|
5
|
-
|
7
|
+
db = Sequel.connect(ENV['DATABASE_URL'] || 'sqlite://db/development.db')
|
6
8
|
|
7
9
|
desc 'Migrate the database to latest version'
|
8
10
|
task :migrate do
|
9
|
-
Sequel::Migrator.run(
|
11
|
+
Sequel::Migrator.run(db, 'db/migrate')
|
10
12
|
puts '<= db:migrate executed'
|
11
13
|
end
|
12
14
|
|
13
15
|
desc 'Perform migration reset (full erase and migration up)'
|
14
16
|
task :reset do
|
15
|
-
Sequel::Migrator.run(
|
16
|
-
Sequel::Migrator.run(
|
17
|
+
Sequel::Migrator.run(db, 'db/migrate', target: 0)
|
18
|
+
Sequel::Migrator.run(db, 'db/migrate')
|
17
19
|
puts '<= db:reset executed'
|
18
20
|
end
|
19
21
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
set -euo pipefail
|
3
|
+
IFS=$'\n\t'
|
4
|
+
|
5
|
+
if gem list foreman -i
|
6
|
+
then
|
7
|
+
echo 'Foreman detected... starting server with Procfile'
|
8
|
+
rerun --background foreman start
|
9
|
+
else
|
10
|
+
echo 'Foreman not installed... starting server with rackup'
|
11
|
+
rerun --background rackup
|
12
|
+
fi
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable RSpec/DescribeClass
|
4
|
+
RSpec.describe 'GET /', rack: true do
|
5
|
+
before { get '/' }
|
6
|
+
|
7
|
+
it { expect(last_response).to have_http_status(:ok) }
|
8
|
+
<% if options[:api_only] -%>
|
9
|
+
it { expect(last_response).to have_content_type(:json) }
|
10
|
+
<% else -%>
|
11
|
+
it { expect(last_response).to have_content_type(:html) }
|
12
|
+
<% end -%>
|
13
|
+
it { expect(last_response.body).to include('Smoke test successful!') }
|
14
|
+
end
|
15
|
+
# rubocop:enable RSpec/DescribeClass
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec::Matchers.define :have_http_status do |expected|
|
4
|
+
match do |response|
|
5
|
+
response.status == response_codes[expected]
|
6
|
+
end
|
7
|
+
|
8
|
+
failure_message do |response|
|
9
|
+
'expected last response status to be ' \
|
10
|
+
"#{response_codes.fetch(expected, 'unknown')}, " \
|
11
|
+
"but was #{response.status}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def response_codes
|
15
|
+
{
|
16
|
+
ok: 200,
|
17
|
+
not_authorized: 401,
|
18
|
+
not_found: 404,
|
19
|
+
redirect: 302
|
20
|
+
}.freeze
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
RSpec::Matchers.define :have_content_type do |expected|
|
25
|
+
match do |response|
|
26
|
+
response.content_type == content_types[expected]
|
27
|
+
end
|
28
|
+
|
29
|
+
failure_message do |response|
|
30
|
+
'expected last response to have content type ' \
|
31
|
+
"'#{content_types.fetch(expected, 'unknown')}', " \
|
32
|
+
"but was '#{response.content_type}'"
|
33
|
+
end
|
34
|
+
|
35
|
+
def content_types
|
36
|
+
{
|
37
|
+
html: 'text/html;charset=utf-8',
|
38
|
+
json: 'application/json'
|
39
|
+
}.freeze
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
RSpec::Matchers.define :redirect_to do |expected|
|
44
|
+
match do |response|
|
45
|
+
path(response) == expected
|
46
|
+
end
|
47
|
+
|
48
|
+
failure_message do |response|
|
49
|
+
"expected last response to redirect to '#{expected}', " \
|
50
|
+
"but it redirected to '#{path(response)}'"
|
51
|
+
end
|
52
|
+
|
53
|
+
def path(response)
|
54
|
+
URI(response.location).path
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ENV['RACK_ENV'] = 'test'
|
4
|
+
|
5
|
+
require 'bundler/setup'
|
6
|
+
|
7
|
+
require 'warning'
|
8
|
+
|
9
|
+
# Ignore all warnings in Gem dependencies
|
10
|
+
Gem.path.each { |path| Warning.ignore(//, path) }
|
11
|
+
|
12
|
+
require 'dotenv'
|
13
|
+
require 'rack/test'
|
14
|
+
require 'support/rack_helpers'
|
15
|
+
|
16
|
+
require_relative '../app'
|
17
|
+
|
18
|
+
Dotenv.load
|
19
|
+
|
20
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
21
|
+
RSpec.configure do |config|
|
22
|
+
config.include RackHelpers, rack: true
|
23
|
+
|
24
|
+
config.expect_with :rspec do |expectations|
|
25
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
26
|
+
end
|
27
|
+
|
28
|
+
config.mock_with :rspec do |mocks|
|
29
|
+
mocks.verify_partial_doubles = true
|
30
|
+
end
|
31
|
+
|
32
|
+
config.default_formatter = 'doc' if config.files_to_run.one?
|
33
|
+
config.disable_monkey_patching!
|
34
|
+
config.example_status_persistence_file_path = 'spec/examples.txt'
|
35
|
+
config.filter_run_when_matching :focus
|
36
|
+
config.order = :random
|
37
|
+
config.shared_context_metadata_behavior = :apply_to_host_groups
|
38
|
+
config.warnings = true
|
39
|
+
|
40
|
+
Kernel.srand config.seed
|
41
|
+
end
|
@@ -1,33 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
1
4
|
namespace :assets do
|
2
5
|
require 'sprockets'
|
3
6
|
require 'uglifier'
|
4
7
|
require 'yui/compressor'
|
5
|
-
sprockets = Sprockets::Environment.new { |env| env.logger = Logger.new(
|
8
|
+
sprockets = Sprockets::Environment.new { |env| env.logger = Logger.new($stdout) }
|
6
9
|
sprockets.css_compressor = YUI::CssCompressor.new
|
7
|
-
sprockets.js_compressor = :
|
10
|
+
sprockets.js_compressor = Uglifier.new(harmony: true)
|
8
11
|
|
9
|
-
%w
|
12
|
+
%w[assets vendor].each do |f|
|
10
13
|
sprockets.append_path File.expand_path("../../#{f}", __FILE__)
|
11
14
|
end
|
12
15
|
|
13
|
-
output_path = File.expand_path('
|
16
|
+
output_path = File.expand_path('../public', __dir__)
|
14
17
|
|
18
|
+
desc 'Pre-compile CSS assets'
|
15
19
|
task :precompile_css do
|
16
20
|
asset = sprockets['styles.css']
|
17
21
|
outfile = Pathname.new(output_path).join('css/styles.css')
|
18
22
|
FileUtils.mkdir_p outfile.dirname
|
19
23
|
asset.write_to(outfile)
|
20
|
-
puts
|
24
|
+
puts 'successfully compiled css assets'
|
21
25
|
end
|
22
26
|
|
27
|
+
desc 'Pre-compile JavaScript assets'
|
23
28
|
task :precompile_js do
|
24
29
|
asset = sprockets['app.js']
|
25
30
|
outfile = Pathname.new(output_path).join('js/app.js')
|
26
31
|
FileUtils.mkdir_p outfile.dirname
|
27
32
|
asset.write_to(outfile)
|
28
|
-
puts
|
33
|
+
puts 'successfully compiled javascript assets'
|
29
34
|
end
|
30
35
|
|
31
36
|
desc 'precompile all assets'
|
32
|
-
task :
|
37
|
+
task precompile: %i[precompile_css precompile_js]
|
33
38
|
end
|
39
|
+
# rubocop:enable Metrics/BlockLength
|
@@ -1,6 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'sprockets'
|
2
4
|
|
3
5
|
module Middleware
|
6
|
+
# Sprockets Rack middleware.
|
7
|
+
#
|
4
8
|
class SprocketsChain
|
5
9
|
attr_reader :app, :prefix, :sprockets
|
6
10
|
|
@@ -12,15 +16,15 @@ module Middleware
|
|
12
16
|
end
|
13
17
|
|
14
18
|
def call(env)
|
15
|
-
path_info = env[
|
19
|
+
path_info = env['PATH_INFO']
|
16
20
|
if path_info =~ prefix
|
17
|
-
env[
|
21
|
+
env['PATH_INFO'].sub!(prefix, '')
|
18
22
|
sprockets.call(env)
|
19
23
|
else
|
20
24
|
app.call(env)
|
21
25
|
end
|
22
26
|
ensure
|
23
|
-
env[
|
27
|
+
env['PATH_INFO'] = path_info
|
24
28
|
end
|
25
29
|
end
|
26
30
|
end
|
@@ -1,13 +1,17 @@
|
|
1
|
-
|
2
|
-
module Sprockets
|
3
|
-
def stylesheet_tag(name)
|
4
|
-
folder = 'production' == ENV['RACK_ENV'] ? 'css' : 'assets'
|
5
|
-
"<link href='/#{folder}/#{name}.css' rel='stylesheet' type='text/css' />"
|
6
|
-
end
|
1
|
+
# frozen_string_literal: true
|
7
2
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
module Helpers
|
4
|
+
# Sprockets view helpers.
|
5
|
+
#
|
6
|
+
module Sprockets
|
7
|
+
def stylesheet_tag(name)
|
8
|
+
folder = 'production' == ENV['RACK_ENV'] ? 'css' : 'assets'
|
9
|
+
"<link href='/#{folder}/#{name}.css' rel='stylesheet' type='text/css' />"
|
10
|
+
end
|
11
|
+
|
12
|
+
def javascript_tag(name)
|
13
|
+
folder = 'production' == ENV['RACK_ENV'] ? 'js' : 'assets'
|
14
|
+
"<script type='text/javascript' src='/#{folder}/#{name}.js'></script>"
|
12
15
|
end
|
13
16
|
end
|
17
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
<% if 'test-unit' == options[:test_framework] -%>
|
4
|
+
require_relative 'rack_test_assertions'
|
5
|
+
<% end -%>
|
6
|
+
<% if 'rspec' == options[:test_framework] -%>
|
7
|
+
require_relative 'rack_matchers'
|
8
|
+
<% end -%>
|
9
|
+
|
10
|
+
# Helpers for running Rack-based tests or specs. Decorates normal Rack methods
|
11
|
+
# with an optional `:as` parameter. If present, the `rack.session` will
|
12
|
+
# include the value of the given user's ID, effectively signing them in for
|
13
|
+
# the request.
|
14
|
+
#
|
15
|
+
# Example
|
16
|
+
# ```
|
17
|
+
# # Find or create a user. You can name the class anything you'd like, the
|
18
|
+
# # only requirement is that it responds to a `id` method.
|
19
|
+
# user = User.new(id: 1) # or User.find, etc.
|
20
|
+
# get '/', as: user
|
21
|
+
# ```
|
22
|
+
#
|
23
|
+
# The above will add a `current_user` to the `rack.session` with a value of
|
24
|
+
# the user's ID.
|
25
|
+
#
|
26
|
+
module RackHelpers
|
27
|
+
include Rack::Test::Methods
|
28
|
+
<% if 'test-unit' == options[:test_framework] -%>
|
29
|
+
include Rack::Test::Assertions
|
30
|
+
<% end -%>
|
31
|
+
|
32
|
+
%w[get post put patch delete options].each do |type|
|
33
|
+
define_method(type) do |uri, params={}, env={}, &block|
|
34
|
+
extract_user!(params, env)
|
35
|
+
super uri, params, env, &block
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def extract_user!(params, env)
|
42
|
+
user = params.delete(:as)
|
43
|
+
return unless user
|
44
|
+
|
45
|
+
if env.key?('rack.session')
|
46
|
+
env['rack.session'].merge!(current_user: user.id)
|
47
|
+
else
|
48
|
+
env.merge!({ 'rack.session' => { current_user: user.id } })
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def app
|
53
|
+
Sinatra::Application
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Test
|
5
|
+
module Assertions
|
6
|
+
RESPONSE_CODES = {
|
7
|
+
ok: 200,
|
8
|
+
not_authorized: 401,
|
9
|
+
not_found: 404,
|
10
|
+
redirect: 302
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
CONTENT_TYPES = {
|
14
|
+
json: 'application/json',
|
15
|
+
html: 'text/html;charset=utf-8'
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def assert_body_contains(expected, message=nil)
|
19
|
+
msg = build_message(
|
20
|
+
message,
|
21
|
+
"expected body to contain <?>\n#{last_response.body}",
|
22
|
+
expected
|
23
|
+
)
|
24
|
+
|
25
|
+
assert_block(msg) { last_response.body.include?(expected) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def assert_content_type(content_type)
|
29
|
+
unless CONTENT_TYPES.keys.include?(content_type)
|
30
|
+
raise ArgumentError, "unrecognized content_type (#{content_type})"
|
31
|
+
end
|
32
|
+
|
33
|
+
assert_equal CONTENT_TYPES[content_type], last_response.content_type
|
34
|
+
end
|
35
|
+
|
36
|
+
def assert_flash(type=:notice, message=nil)
|
37
|
+
msg = build_message(
|
38
|
+
message,
|
39
|
+
'expected <?> flash to exist, but was nil',
|
40
|
+
type.to_s
|
41
|
+
)
|
42
|
+
|
43
|
+
assert_block(msg) { last_request.env['rack.session']['flash'] }
|
44
|
+
end
|
45
|
+
|
46
|
+
def assert_flash_message(expected, type=:notice, message=nil)
|
47
|
+
assert_flash(type, message)
|
48
|
+
flash = last_request.env['rack.session']['flash'][type.to_s]
|
49
|
+
|
50
|
+
msg = build_message(
|
51
|
+
message,
|
52
|
+
'expected flash to be <?> but was <?>',
|
53
|
+
expected,
|
54
|
+
flash
|
55
|
+
)
|
56
|
+
|
57
|
+
assert_block(msg) { expected == flash }
|
58
|
+
end
|
59
|
+
|
60
|
+
def assert_has_session(message=nil)
|
61
|
+
msg = build_message(message, 'expected a valid session')
|
62
|
+
assert_block(msg) { last_request.env['rack.session'] }
|
63
|
+
end
|
64
|
+
|
65
|
+
def assert_session_has_key(key, message=nil)
|
66
|
+
assert_has_session
|
67
|
+
msg = build_message(message, 'expected session to have key named <?>', key)
|
68
|
+
assert_block(msg) { last_request.env['rack.session'].keys.include?(key.to_s) }
|
69
|
+
end
|
70
|
+
|
71
|
+
def assert_session(key, expected, message=nil)
|
72
|
+
assert_session_has_key(key)
|
73
|
+
actual = last_request.env['rack.session'][key.to_s]
|
74
|
+
msg = build_message(
|
75
|
+
message,
|
76
|
+
'expected session key <?> to be <?>, but was <?>',
|
77
|
+
key,
|
78
|
+
expected,
|
79
|
+
actual
|
80
|
+
)
|
81
|
+
|
82
|
+
assert_block(msg) { expected == actual }
|
83
|
+
end
|
84
|
+
|
85
|
+
def assert_response(expected, message=nil)
|
86
|
+
status = last_response.status
|
87
|
+
msg = build_message(
|
88
|
+
message,
|
89
|
+
'expected last response to be <?> but was <?>',
|
90
|
+
"#{RESPONSE_CODES[expected]}:#{expected}",
|
91
|
+
"#{status}:#{RESPONSE_CODES.key(status)}"
|
92
|
+
)
|
93
|
+
|
94
|
+
assert_block(msg) { status == RESPONSE_CODES[expected] }
|
95
|
+
end
|
96
|
+
|
97
|
+
def assert_redirected_to(expected, message=nil)
|
98
|
+
assert_response(:redirect)
|
99
|
+
actual = URI(last_response.location).path
|
100
|
+
msg = build_message(
|
101
|
+
message,
|
102
|
+
'expected to be redirected to <?> but was <?>',
|
103
|
+
expected,
|
104
|
+
actual
|
105
|
+
)
|
106
|
+
|
107
|
+
assert_block(msg) { expected == actual }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|