diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..83e16f80 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..4470a83a --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,8 @@ +AllCops: + DisplayCopNames: true + DisplayStyleGuide: true + ExtraDetails: false +Style/StringLiterals: + Enabled: false +Metrics/LineLength: + Enabled: false diff --git a/Gemfile b/Gemfile index b0caa105..65c3735a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' gemspec -if !ENV['TRAVIS'] +unless ENV['TRAVIS'] gem 'byebug', require: false, platforms: :mri gem 'pry', require: false, platforms: :jruby gem 'yard', require: false @@ -31,7 +31,7 @@ platforms :jruby do gem 'jdbc-sqlite3' end -gem 'coveralls', require: false - -gem 'dotenv', '~> 2.0' -gem 'shotgun', '~> 0.9' +gem 'dotenv', '~> 2.0', require: false +gem 'shotgun', '~> 0.9', require: false +gem 'rubocop', '~> 0.43.0', require: false +gem 'coveralls', require: false diff --git a/Rakefile b/Rakefile index 74243f10..d657feba 100644 --- a/Rakefile +++ b/Rakefile @@ -1,18 +1,14 @@ require 'rake' -require 'rake/testtask' require 'bundler/gem_tasks' +require 'rspec/core/rake_task' -Rake::TestTask.new do |t| - t.pattern = 'test/**/*_test.rb' - t.libs.push 'test' -end +RSpec::Core::RakeTask.new(:spec) namespace :test do task :coverage do ENV['COVERAGE'] = 'true' - Rake::Task['test'].invoke + Rake::Task['spec'].invoke end end -task default: :test - +task default: :spec diff --git a/hanami.gemspec b/hanami.gemspec index 2c435d6b..2b80bd76 100644 --- a/hanami.gemspec +++ b/hanami.gemspec @@ -8,8 +8,8 @@ Gem::Specification.new do |spec| spec.version = Hanami::VERSION spec.authors = ['Luca Guidi', 'Trung LĂȘ', 'Alfonso Uceda Pompa'] spec.email = ['me@lucaguidi.com', 'trung.le@ruby-journal.com', 'uceda73@gmail.com'] - spec.summary = %q{The web, with simplicity.} - spec.description = %q{Hanami is a web framework for Ruby} + spec.summary = 'The web, with simplicity' + spec.description = 'Hanami is a web framework for Ruby' spec.homepage = 'http://hanamirb.org' spec.license = 'MIT' @@ -17,7 +17,9 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test)/}) spec.require_paths = ['lib'] - spec.required_ruby_version = '>= 2.2.0' + spec.required_ruby_version = '>= 2.3.0' + + spec.metadata['allowed_push_host'] = 'https://rubygems.org' spec.add_dependency 'hanami-utils', '~> 0.8' spec.add_dependency 'hanami-validations', '~> 0.6' @@ -28,10 +30,9 @@ Gem::Specification.new do |spec| spec.add_dependency 'hanami-mailer', '~> 0.3' spec.add_dependency 'hanami-assets', '~> 0.3' spec.add_dependency 'thor', '~> 0.19' - spec.add_dependency 'bundler', '~> 1.6' + spec.add_dependency 'bundler', '~> 1.13' - spec.add_development_dependency 'minispec-metadata', '~> 3.2.1' - spec.add_development_dependency 'minitest', '~> 5' - spec.add_development_dependency 'rack-test', '~> 0.6' - spec.add_development_dependency 'rake', '~> 10' + spec.add_development_dependency 'rspec', '~> 3.5' + spec.add_development_dependency 'rack-test', '~> 0.6' + spec.add_development_dependency 'rake', '~> 11.3' end diff --git a/lib/hanami/env.rb b/lib/hanami/env.rb new file mode 100644 index 00000000..7aecde4a --- /dev/null +++ b/lib/hanami/env.rb @@ -0,0 +1,65 @@ +begin + require 'dotenv' +rescue LoadError # rubocop:disable Lint/HandleExceptions +end + +module Hanami + # Encapsulate access to ENV + # + # @since x.x.x + # @api private + class Env + # Create a new instance + # + # @param env [#[],#[]=] a Hash like object. It defaults to ENV + # + # @return [Hanami::Env] + # + # @since x.x.x + # @api private + def initialize(env: ENV) + @env = env + end + + # Return a value, if found + # + # @param key [String] the key + # + # @return [String,NilClass] the value, if found + # + # @since x.x.x + # @api private + def [](key) + @env[key] + end + + # Sets a value + # + # @param key [String] the key + # @param value [String] the value + # + # @since x.x.x + # @api private + def []=(key, value) + @env[key] = value + end + + # Loads a dotenv file and updates self + # + # @param path [String, Pathname] the path to the dotenv file + # + # @return void + # + # @since x.x.x + # @api private + def load!(path) + return unless defined?(Dotenv) + + contents = ::File.open(path, "rb:bom|utf-8", &:read) + parsed = Dotenv::Parser.call(contents) + + @env.merge!(parsed) + nil + end + end +end diff --git a/lib/hanami/environment.rb b/lib/hanami/environment.rb index c6e074b7..28f905d5 100644 --- a/lib/hanami/environment.rb +++ b/lib/hanami/environment.rb @@ -2,11 +2,8 @@ require 'thread' require 'pathname' require 'hanami/utils' require 'hanami/utils/hash' +require 'hanami/env' require 'hanami/hanamirc' -begin - require 'dotenv' -rescue LoadError -end module Hanami # Define and expose information about the Hanami environment. @@ -196,6 +193,7 @@ module Hanami # # the one defined in the parent (eg `FOO` is overwritten). All the # # other settings (eg `XYZ`) will be left untouched. def initialize(options = {}) + @env = Hanami::Env.new(env: options.delete(:env) || ENV) @options = Hanami::Hanamirc.new(root).options @options.merge! Utils::Hash.new(options.clone).symbolize! LOCK.synchronize { set_env_vars! } @@ -218,7 +216,7 @@ module Hanami # # @see Hanami::Environment::DEFAULT_ENV def environment - @environment ||= ENV[HANAMI_ENV] || rack_env || DEFAULT_ENV + @environment ||= env[HANAMI_ENV] || rack_env || DEFAULT_ENV end # @since 0.3.1 @@ -304,9 +302,9 @@ module Hanami # @see Hanami::Environment::DEFAULT_HOST # @see Hanami::Environment::LISTEN_ALL_HOST def host - @host ||= @options.fetch(:host) { - ENV[HANAMI_HOST] || default_host - } + @host ||= @options.fetch(:host) do + env[HANAMI_HOST] || default_host + end end # The HTTP port @@ -324,7 +322,9 @@ module Hanami # # @see Hanami::Environment::DEFAULT_PORT def port - @port ||= @options.fetch(:port) { ENV[HANAMI_PORT] || DEFAULT_PORT }.to_i + @port ||= @options.fetch(:port) do + env[HANAMI_PORT] || DEFAULT_PORT + end.to_i end # Path to the Rack configuration file @@ -401,10 +401,10 @@ module Hanami # @since 0.4.0 # @api private def architecture - @options.fetch(:architecture) { + @options.fetch(:architecture) do puts "Cannot recognize Hanami architecture, please check `.hanamirc'" exit 1 - } + end end # @since 0.4.0 @@ -416,7 +416,7 @@ module Hanami # @since 0.6.0 # @api private def serve_static_assets? - SERVE_STATIC_ASSETS_ENABLED == ENV[SERVE_STATIC_ASSETS] + SERVE_STATIC_ASSETS_ENABLED == env[SERVE_STATIC_ASSETS] end # @since 0.6.0 @@ -463,6 +463,8 @@ module Hanami private + attr_reader :env + # @since 0.1.0 # @api private def set_env_vars! @@ -473,16 +475,18 @@ module Hanami # @since 0.2.0 # @api private def set_hanami_env_vars! - ENV[HANAMI_ENV] = ENV[RACK_ENV] = environment - ENV[HANAMI_HOST] = host - ENV[HANAMI_PORT] = port.to_s + env[HANAMI_ENV] = env[RACK_ENV] = environment + env[HANAMI_HOST] = host + env[HANAMI_PORT] = port.to_s end # @since 0.2.0 # @api private def set_application_env_vars! - return unless defined?(Dotenv) && (dotenv = root.join(DEFAULT_DOTENV_ENV % environment)).exist? - Dotenv.overload dotenv + dotenv = root.join(DEFAULT_DOTENV_ENV % environment) + return unless dotenv.exist? + + env.load!(dotenv) end # @since 0.1.0 @@ -494,11 +498,11 @@ module Hanami # @since 0.6.0 # @api private def rack_env - case ENV[RACK_ENV] + case env[RACK_ENV] when RACK_ENV_DEPLOYMENT PRODUCTION_ENV else - ENV[RACK_ENV] + env[RACK_ENV] end end end diff --git a/lib/hanami/version.rb b/lib/hanami/version.rb index a9c05476..48698ad8 100644 --- a/lib/hanami/version.rb +++ b/lib/hanami/version.rb @@ -2,5 +2,5 @@ module Hanami # Defines the full version # # @since 0.1.0 - VERSION = '0.8.0'.freeze + VERSION = '1.0.0.alpha1'.freeze end diff --git a/spec/env_spec.rb b/spec/env_spec.rb new file mode 100644 index 00000000..b7d96325 --- /dev/null +++ b/spec/env_spec.rb @@ -0,0 +1,30 @@ +RSpec.describe Hanami::Env do + after do + ENV['HANAMI_ENV_TEST_VARIABLE'] = nil + end + + describe "#[]" do + it "reads value from ENV" do + expect(described_class.new['PATH']).to eq(ENV['PATH']) + end + end + + describe "#[]=" do + it "sets value to ENV" do + subject = described_class.new + subject['HANAMI_ENV_TEST_VARIABLE'] = 'foo' + + expect(subject['HANAMI_ENV_TEST_VARIABLE']).to eq(ENV['HANAMI_ENV_TEST_VARIABLE']) + end + end + + describe "#load!" do + it "loads env vars" do + env = {} + subject = described_class.new(env: env) + + subject.load!('spec/fixtures/dotenv/.env.development') + expect(subject['BAZ']).to eq('yes') + end + end +end diff --git a/spec/environment_spec.rb b/spec/environment_spec.rb new file mode 100644 index 00000000..e3b779b8 --- /dev/null +++ b/spec/environment_spec.rb @@ -0,0 +1,308 @@ +RSpec.describe Hanami::Environment do + let(:env) { Hash[] } + + let(:default_development_env) do + Hash[ + 'RACK_ENV' => 'development', + 'HANAMI_ENV' => 'development', + 'HANAMI_HOST' => 'localhost', + 'HANAMI_PORT' => '2300' + ] + end + + let(:default_test_env) do + Hash[ + 'RACK_ENV' => 'test', + 'HANAMI_ENV' => 'test', + 'HANAMI_HOST' => '0.0.0.0', + 'HANAMI_PORT' => '2300' + ] + end + + describe "#initialize" do + context "global .env" do + it "doesn't set env vars from .env" do + with_directory('spec/fixtures') do + described_class.new(env: env) + + expect(env['FOO']).to be_nil # see spec/fixtures/.env + end + end + + it "doesn't sets port" do + with_directory('spec/fixtures') do + subject = described_class.new(env: env) + + # returns default instead the value from spec/fixtures/.env + expect(subject.port).to eq(2300) + end + end + end + + context "per environment .env" do + it "sets env vars from .env.development" do + with_directory('spec/fixtures/dotenv') do + described_class.new(env: env) + + expect(env['HANAMI_PORT']).to eq('42') + + expect(env['BAZ']).to eq('yes') + expect(env['WAT']).to eq('true') + end + end + + it "sets port from .env.development" do + with_directory('spec/fixtures/dotenv') do + subject = described_class.new(env: env) + + expect(subject.port).to eq(42) + end + end + end + + context "missing per environment .env" do + it "doesn't alter env" do + with_directory('spec/fixtures/nodotenv') do + described_class.new(env: env) + + expect(env).to eq(default_development_env) + end + end + end + + context "when the .env for the current environment is missing" do + let(:env) { Hash['HANAMI_ENV' => 'test'] } + + it "doesn't set env vars" do + with_directory('spec/fixtures/dotenv') do + described_class.new(env: env) + + expect(env).to eq(default_test_env) + end + end + end + end # initialize + + describe "#environment" do + context "when HANAMI_ENV isn't set" do + it "returns 'development'" do + subject = described_class.new(env: env) + + expect(subject.environment).to eq('development') + end + end + + context "when HANAMI_ENV is set" do + let(:env) { Hash['HANAMI_ENV' => 'test'] } + + it "returns that value" do + subject = described_class.new(env: env) + + expect(subject.environment).to eq('test') + end + end + + context "when RACK_ENV is set to 'production'" do + let(:env) { Hash['RACK_ENV' => 'production'] } + + it "returns that value" do + subject = described_class.new(env: env) + + expect(subject.environment).to eq('production') + end + end + + context "when RACK_ENV is set to 'deployment'" do + let(:env) { Hash['RACK_ENV' => 'deployment'] } + + it "returns 'production'" do + subject = described_class.new(env: env) + + expect(subject.environment).to eq('production') + end + end + + context "when both HANAMI_ENV and RACK_ENV are set" do + let(:env) { Hash['HANAMI_ENV' => 'test', 'RACK_ENV' => 'production'] } + + it "gives precedence to HANAMI_ENV" do + subject = described_class.new(env: env) + + expect(subject.environment).to eq('test') + end + end + + context "when env vars are changed from different process" do + it "doesn't change value after initialization" do + subject = described_class.new(env: env) + + expect(subject.environment).to eq('development') + + env['HANAMI_ENV'] = 'production' + expect(subject.environment).to eq('development') + end + end + end # environment + + describe "#environment?" do + subject { described_class.new(env: env) } + + context "when matched" do + let(:env) { Hash['HANAMI_ENV' => 'test'] } + + context "with single name" do + it "returns true" do + expect(subject.environment?(:test)).to be(true) + expect(subject.environment?('test')).to be(true) + end + end + + context "with multiple names" do + it "returns true" do + expect(subject.environment?(:development, :test)).to be(true) + expect(subject.environment?('development', 'test')).to be(true) + end + end + end + + context "when not matched" do + let(:env) { Hash['HANAMI_ENV' => 'development'] } + + context "with single name" do + it "returns false" do + expect(subject.environment?(:test)).to be(false) + expect(subject.environment?('test')).to be(false) + end + end + + context "with multiple names" do + it "returns false" do + expect(subject.environment?(:test, :production)).to be(false) + expect(subject.environment?('test', 'production')).to be(false) + end + end + end + end # environment? + + describe "#bundler_groups" do + it "returns a set of groups for Bundler" do + subject = described_class.new(env: env) + + expect(subject.bundler_groups).to eq([:default, subject.environment]) + end + end # bundler_groups + + describe "#host" do + context "when not specified" do + context "and default env" do + it "returns 'localhost'" do + subject = described_class.new(env: env) + + expect(subject.host).to eq('localhost') + end + end + + context "and other env" do + let(:env) { Hash['HANAMI_ENV' => 'test'] } + + it "returns '0.0.0.0'" do + subject = described_class.new(env: env) + + expect(subject.host).to eq('0.0.0.0') + end + end + end + + context "when specified while initializing" do + it 'returns that value' do + subject = described_class.new(env: env, host: host = 'hanamirb.test') + + expect(subject.host).to eq(host) + end + end + + context "when HANAMI_HOST is set" do + let(:env) { Hash['HANAMI_HOST' => host] } + let(:host) { 'hanami.host' } + + it 'returns that value' do + subject = described_class.new(env: env) + + expect(subject.host).to eq(host) + end + end + + context "when both the option and HANAMI_HOST are set" do + let(:env) { Hash['HANAMI_HOST' => 'hanami.host'] } + + it 'returns that value' do + subject = described_class.new(env: env, host: host = 'hanamirb.org') + + expect(subject.host).to eq(host) + end + end + end # host + + describe "#port" do + context "when not specified" do + it "returns 2300" do + subject = described_class.new(env: env) + + expect(subject.port).to eq(2300) + end + end + + context "when specified while initializing" do + it 'returns that value' do + subject = described_class.new(env: env, port: port = 9292) + + expect(subject.port).to eq(port) + end + end + + context "when HANAMI_PORT is set" do + let(:env) { Hash['HANAMI_PORT' => port] } + let(:port) { 8080 } + + it 'returns that value' do + subject = described_class.new(env: env) + + expect(subject.port).to eq(port) + end + end + + context "when both the option and HANAMI_PORT are set" do + let(:env) { Hash['HANAMI_PORT' => 8081] } + + it 'returns that value' do + subject = described_class.new(env: env, port: port = 9393) + + expect(subject.port).to eq(port) + end + end + end # port + + describe "#project_name" do + it "equals to given value" do + subject = described_class.new(env: env, project: project = 'bookshelf') + + expect(subject.project_name).to eq(project) + end + end # project_name + + describe "#root" do + it "equals to Dir.pwd" do + subject = described_class.new(env: env) + + expect(subject.root).to eq(Pathname(Dir.pwd)) + end + end # root + + describe "#rackup" do + it "is 'config.ru' at root" do + subject = described_class.new(env: env) + + expect(subject.rackup).to eq(subject.root.join('config.ru')) + end + end # rackup +end diff --git a/spec/fixtures/.env b/spec/fixtures/.env new file mode 100644 index 00000000..5ab08dc1 --- /dev/null +++ b/spec/fixtures/.env @@ -0,0 +1,3 @@ +FOO="bar" +BAZ="no" +HANAMI_PORT=42 diff --git a/spec/fixtures/dotenv/.byebug_history b/spec/fixtures/dotenv/.byebug_history new file mode 100644 index 00000000..698cb94c --- /dev/null +++ b/spec/fixtures/dotenv/.byebug_history @@ -0,0 +1,38 @@ +c +n +env +n +dotenv +n +step +@env +@options +n +c +@options +n +@env +n +options +c +n +step +n +step +c +n +step +env +c +ENV['HANAMI_PORT'] +ENV.keys +n +dotenv +n +Dotenv +step +@options +n +@env +n +step diff --git a/spec/fixtures/dotenv/.env.development b/spec/fixtures/dotenv/.env.development new file mode 100644 index 00000000..4654f61e --- /dev/null +++ b/spec/fixtures/dotenv/.env.development @@ -0,0 +1,3 @@ +HANAMI_PORT=42 +BAZ="yes" +WAT="true" diff --git a/spec/fixtures/nodotenv/.gitkeep b/spec/fixtures/nodotenv/.gitkeep new file mode 100644 index 00000000..77f2af2a --- /dev/null +++ b/spec/fixtures/nodotenv/.gitkeep @@ -0,0 +1 @@ +gitkeep diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..09928150 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,23 @@ +RSpec.configure do |config| + config.expect_with :rspec do |expectations| + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + config.mock_with :rspec do |mocks| + mocks.verify_partial_doubles = true + end + + config.shared_context_metadata_behavior = :apply_to_host_groups + + config.filter_run_when_matching :focus + config.disable_monkey_patching! + config.warnings = true + + config.default_formatter = 'doc' if config.files_to_run.one? + + config.order = :random + Kernel.srand config.seed +end + +Dir[__dir__ + '/support/**/*.rb'].each { |f| require_relative f } +require 'hanami' diff --git a/spec/support/with_directory.rb b/spec/support/with_directory.rb new file mode 100644 index 00000000..0f9010ec --- /dev/null +++ b/spec/support/with_directory.rb @@ -0,0 +1,21 @@ +require 'pathname' + +module RSpec + module WithDirectory + private + + def with_directory(directory) + current = Dir.pwd + target = Pathname.new(Dir.pwd).join(directory) + + Dir.chdir(target) + yield + ensure + Dir.chdir(current) + end + end +end + +RSpec.configure do |config| + config.include RSpec::WithDirectory +end diff --git a/spec/version_spec.rb b/spec/version_spec.rb new file mode 100644 index 00000000..750d0bb4 --- /dev/null +++ b/spec/version_spec.rb @@ -0,0 +1,5 @@ +RSpec.describe Hanami::VERSION do + it 'returns current version' do + expect(subject).to eq('1.0.0.alpha1') + end +end