env_lint 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7074acbb267701af05f8e2f5307fb5d8f563a1dc
4
+ data.tar.gz: 1f07d11602f9e8230159f83a0719b3cfdb503047
5
+ SHA512:
6
+ metadata.gz: f1895359dbf17fca012f7ed52ff250ba0ea60b0edfd493691aabbf7b424f4d209d4b8d07c747a9b91e856f6e30f8b478c6fc07462f1592e6ba0a5c720226ae32
7
+ data.tar.gz: 564fa51150c32279cfcdc4fc965dd0e6b72c1c90d75b9c8ec33fe516394cb8c26fce7034c39d86446c24830275d624689ca4b6caf9ed4c9253df15b2add49573
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ rvm:
2
+ - 1.9.3
3
+ - 2.0.0
4
+ - 2.1.0
5
+
6
+ script: "bundle exec rspec"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in env_lint.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Tim Fischbach
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # Env Lint
2
+
3
+ <img src="https://travis-ci.org/tf/env_lint.png" data-bindattr-466="466" title="Build Status Images">
4
+
5
+ Check environment variables accoring to a `.env.example` file.
6
+
7
+ * Avoid spelling errors in variable names in your code or on the
8
+ command line
9
+ * Ensure all relevant environment variables are described in the
10
+ `.env.example` file.
11
+ * Ensure all required environment variables are configured before
12
+ deploying a new version of an app
13
+ * Ease setting up a new development machine
14
+
15
+ # Status
16
+
17
+ Only tested with [recap](https://github.com/tomafro/recap) capistrano
18
+ tasks.
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ gem 'env_lint'
25
+
26
+ ## Usage
27
+
28
+ Define a `.env.example` file:
29
+
30
+ # Explain each variable in comments like this one
31
+ APP_NAME=my_app
32
+
33
+ # Comments are also recognized if they span multiple
34
+ # lines
35
+ FEATURE=true
36
+
37
+ # Optional variables
38
+ # OPTIONAL_VAR="set me if you like"
39
+
40
+ ### Rake Task
41
+
42
+ Require it in your `Rakefile`:
43
+
44
+ require 'env_lint/tasks'
45
+
46
+ Now you can check your environment:
47
+
48
+ $ rake env:lint
49
+ => Complains if non optional variables are missing
50
+
51
+ If special steps are needed
52
+
53
+ ### Capistrano Task
54
+
55
+ Require it in your `Capfile`:
56
+
57
+ require 'env_lint/capistrano'
58
+
59
+ Now you can check your servers:
60
+
61
+ $ cap env:lint
62
+ => Complains if non optional variables are missing
63
+
64
+ You might want to lint the environment automatically before each
65
+ deploy.
66
+
67
+ before 'deploy', 'env:lint'
68
+
69
+ Lint variable names before setting them:
70
+
71
+ before 'env:set', 'env:lint_args'
72
+
73
+ $ cap env:set APP_NAME=myapp
74
+ => Complains if APP_NAME is defined
75
+
76
+ ### Lint at Runtime
77
+
78
+ Access ENV through a `LintedEnv`:
79
+
80
+ require 'env_lint'
81
+
82
+ class MyApp
83
+ LINTED_ENV = EnvLint::LintedEnv.from_file('.env.example')
84
+
85
+ def self.env
86
+ LINTED_ENV
87
+ end
88
+ end
89
+
90
+ Accessing env variables:
91
+
92
+ # Ensures APP_NAME is defined in .env.example
93
+ MyApp.env.fetch(:app_name, 'App name')
94
+
95
+ # Ensures APP_NAME is non optional in .env.example
96
+ MyApp.env.fetch(:app_name)
97
+
98
+ ## Contributing
99
+
100
+ 1. Fork it
101
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
102
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
103
+ 4. Push to the branch (`git push origin my-new-feature`)
104
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/env_lint.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'env_lint/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'env_lint'
8
+ spec.version = EnvLint::VERSION
9
+ spec.authors = ['Tim Fischbach']
10
+ spec.email = ['mail@timfischbach.de']
11
+ spec.summary = 'Lint the environment according by .env.example'
12
+ spec.homepage = ''
13
+ spec.license = 'MIT'
14
+
15
+ spec.files = `git ls-files`.split($/)
16
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
17
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
+ spec.require_paths = ['lib']
19
+
20
+ spec.add_runtime_dependency 'formatador'
21
+ spec.add_runtime_dependency 'capistrano', '~> 2.9'
22
+
23
+ spec.add_development_dependency 'rspec', '3.0.0.beta1'
24
+ spec.add_development_dependency 'bundler', '~> 1.3'
25
+ spec.add_development_dependency 'rake'
26
+ end
@@ -0,0 +1,51 @@
1
+ require 'env_lint'
2
+ require 'capistrano'
3
+
4
+ module EnvLint
5
+ module Capistrano
6
+ def self.load_into(config, formatter)
7
+ config.load do
8
+ set(:env_definition_file) { '.env.example' }
9
+ set(:env_probe_command) { "su - #{application_user} -c 'export'" }
10
+
11
+ namespace :env do
12
+ desc 'Check that every non optional ENV variable is defined.'
13
+ task :lint do
14
+ begin
15
+ EnvLint.verify_export_output(env_definition_file, capture(env_probe_command, via: :sudo))
16
+ formatter.ok('env looks ok')
17
+ rescue EnvLint::MissingVariables => e
18
+ formatter.missing_variables(e.dot_env_file, e.missing_variables)
19
+ abort
20
+ rescue EnvLint::Error => e
21
+ formatter.error(e.message)
22
+ abort
23
+ end
24
+ end
25
+
26
+ desc 'Lint args passed to command.'
27
+ task :lint_args do
28
+ begin
29
+ EnvLint.verify_args(env_definition_file, env_args)
30
+ rescue EnvLint::UnknownVariables => e
31
+ formatter.unknown_variables(e.dot_env_file, e.unknown_variables)
32
+ abort
33
+ rescue EnvLint::Error => e
34
+ formatter.error(e.message)
35
+ abort
36
+ end
37
+ end
38
+
39
+ def env_args
40
+ ARGV[1..-1]
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+
48
+ if Capistrano::Configuration.instance
49
+ EnvLint::Capistrano.load_into(Capistrano::Configuration.instance(:must_exist),
50
+ Formatter.new)
51
+ end
@@ -0,0 +1,48 @@
1
+ module EnvLint
2
+ class DotEnvFile
3
+ attr_reader :name, :variables
4
+
5
+ def initialize(name, variables)
6
+ @name = name
7
+ @variables = variables
8
+
9
+ @variables_by_name = variables.each_with_object({}) do |variable, hash|
10
+ hash[variable.name] = variable
11
+ end
12
+ end
13
+
14
+ def find_variable(name)
15
+ @variables_by_name[name]
16
+ end
17
+
18
+ def verify_no_missing(variable_names)
19
+ find_missing(variable_names).tap do |missing_variables|
20
+ raise(MissingVariables.new(self, missing_variables)) if missing_variables.any?
21
+ end
22
+ end
23
+
24
+ def verify_no_unknown(variable_names)
25
+ find_unknown(variable_names).tap do |unknown_names|
26
+ raise(UnknownVariables.new(self, unknown_names)) if unknown_names.any?
27
+ end
28
+ end
29
+
30
+ def self.from_file(file_name)
31
+ new(file_name, DotEnvParser.new.parse(File.read(file_name)))
32
+ end
33
+
34
+ private
35
+
36
+ def find_missing(variable_names)
37
+ @variables.find_all do |variable|
38
+ !variable.optional? && !variable_names.include?(variable.name)
39
+ end
40
+ end
41
+
42
+ def find_unknown(variable_names)
43
+ variable_names.find_all do |name|
44
+ !@variables_by_name.key?(name)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,39 @@
1
+ module EnvLint
2
+ class DotEnvParser
3
+ def parse(text)
4
+ comment_lines = []
5
+
6
+ variables = text.lines.each_with_object([]) do |line, result|
7
+ if match = line.strip.match(ASSIGNMENT)
8
+ optional, name, value = match.captures
9
+ value ||= ''
10
+ value = value.strip.sub(/\A(['"])(.*)\1\z/, '\2')
11
+
12
+ result << Variable.new(name, value, !!optional, comment_lines * "\n")
13
+ comment_lines = []
14
+ elsif match = line.strip.match(COMMENT)
15
+ comment_lines << match.captures.first
16
+ elsif line.strip.empty?
17
+ comment_lines = []
18
+ else
19
+ raise(UnrecognizedDotEnvLine.new(line))
20
+ end
21
+ end
22
+
23
+ variables
24
+ end
25
+
26
+ COMMENT = /\A#\s*(.*)\z/
27
+
28
+ ASSIGNMENT = /
29
+ \A
30
+ (\#\s*)? # optional variable marker
31
+ ([\w\.]+) # key
32
+ = # separator
33
+ ( # optional value begin
34
+ [^#\n]+ # unquoted value
35
+ )? # value end
36
+ \z
37
+ /x
38
+ end
39
+ end
@@ -0,0 +1,11 @@
1
+ module EnvLint
2
+ class EnvKeyParser
3
+ def parse_args(args)
4
+ args.map { |arg| arg.split('=').first if arg.include?('=') }.compact
5
+ end
6
+
7
+ def parse_export_output(text)
8
+ parse_args(text.split(/[\n\r]/).map { |line| line.gsub('declare -x', '').strip })
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,62 @@
1
+ module EnvLint
2
+ class Error < StandardError
3
+ end
4
+
5
+ class UnrecognizedDotEnvLine < Error
6
+ attr_reader :line
7
+
8
+ def initialize(line)
9
+ super("Unrecognized line in dot env file: '#{line}'")
10
+ @line = line
11
+ end
12
+ end
13
+
14
+ class VariableError < StandardError
15
+ attr_reader :variable_name
16
+
17
+ def initialize(variable_name, message)
18
+ super(message)
19
+ @variable_name = variable_name
20
+ end
21
+ end
22
+
23
+ class UnknownVariable < VariableError
24
+ def initialize(variable_name)
25
+ super(variable_name, "Unknown variable #{variable_name}.")
26
+ end
27
+ end
28
+
29
+ class DefaultValueRequiredForOptionalVariable < VariableError
30
+ def initialize(variable_name)
31
+ super(variable_name, "Non optional variable #{variable_name} used without default value.")
32
+ end
33
+ end
34
+
35
+ class MissingVariable < VariableError
36
+ def initialize(variable_name)
37
+ super(variable_name, "Missing variable #{variable_name}. Check your .env file")
38
+ end
39
+ end
40
+
41
+ class MissingVariables < Error
42
+ attr_reader :dot_env_file, :missing_variables
43
+
44
+ def initialize(dot_env_file, missing_variables)
45
+ @dot_env_file = dot_env_file
46
+ @missing_variables = missing_variables
47
+
48
+ super("Missing variables #{missing_variables * ', '}.")
49
+ end
50
+ end
51
+
52
+ class UnknownVariables < Error
53
+ attr_reader :dot_env_file, :unknown_variables
54
+
55
+ def initialize(dot_env_file, unknown_variables)
56
+ @dot_env_file = dot_env_file
57
+ @unknown_variables = unknown_variables
58
+
59
+ super("Unknown variables #{unknown_variables * ', '}.")
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,49 @@
1
+ require 'formatador'
2
+
3
+ module EnvLint
4
+ class Formatter
5
+ def initialize(out = Formatador)
6
+ @out = out
7
+ end
8
+
9
+ def missing_variables(dot_env_file, variables)
10
+ error("Missing env variables:\n")
11
+
12
+ variables.each do |variable|
13
+ @out.display_line(" [yellow]#{variable.name}[/] - #{variable.comment}")
14
+ end
15
+
16
+ new_line
17
+ info("Either set the variable or make it optional in the #{dot_env_file.name} file.")
18
+ end
19
+
20
+ def unknown_variables(dot_env_file, variable_names)
21
+ error("Unknown env variables:\n")
22
+
23
+ variable_names.each do |name|
24
+ @out.display_line(" [yellow]#{name}[/]")
25
+ end
26
+
27
+ new_line
28
+ info("Only variables descibred in #{dot_env_file.name} can be used.")
29
+ end
30
+
31
+ def error(message)
32
+ @out.display_line("* [red]#{message}[/]")
33
+ end
34
+
35
+ def ok(message)
36
+ @out.display_line("* [green]#{message}[/]")
37
+ end
38
+
39
+ private
40
+
41
+ def info(message)
42
+ @out.display_line(" #{message}")
43
+ end
44
+
45
+ def new_line
46
+ @out.display_line('')
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,29 @@
1
+ module EnvLint
2
+ class LintedEnv
3
+ def initialize(env, dot_env_file)
4
+ @env = env
5
+ @dot_env_file = dot_env_file
6
+ end
7
+
8
+ def fetch(name, *args, &block)
9
+ name = name.to_s.upcase if name.is_a?(Symbol)
10
+ variable = @dot_env_file.find_variable(name)
11
+
12
+ raise(UnknownVariable.new(name)) unless variable
13
+
14
+ if variable.optional? && args.empty? && !block_given?
15
+ raise(DefaultValueRequiredForOptionalVariable.new(name))
16
+ end
17
+
18
+ block ||= lambda do |i|
19
+ args.any? ? args.first : raise(MissingVariable.new(name))
20
+ end
21
+
22
+ @env.fetch(name, &block)
23
+ end
24
+
25
+ def self.from_file(file_name)
26
+ new(ENV, DotEnvFile.from_file(file_name))
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ namespace :env do
2
+ desc 'Ensure every non optional ENV variable is defined.'
3
+ task :lint => :load do
4
+ begin
5
+ EnvLint.verify_hash(env_definition_file, ENV)
6
+ EnvLint.formatter.ok('env looks ok')
7
+ rescue EnvLint::MissingVariables => e
8
+ EnvLint.formatter.missing_variables(e.dot_env_file, e.missing_variables)
9
+ abort
10
+ rescue EnvLint::Error => e
11
+ EnvLint.formatter.error(e.message)
12
+ end
13
+ end
14
+
15
+ task :load do
16
+ end
17
+
18
+ def env_definition_file
19
+ ENV['DEFINITION'] || '.env.example'
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ module EnvLint
2
+ class Variable < Struct.new(:name, :value, :optional, :comment)
3
+ alias_method :optional?, :optional
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module EnvLint
2
+ VERSION = "0.0.1"
3
+ end
data/lib/env_lint.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'env_lint/dot_env_file'
2
+ require 'env_lint/dot_env_parser'
3
+ require 'env_lint/formatter'
4
+ require 'env_lint/env_key_parser'
5
+ require 'env_lint/errors'
6
+ require 'env_lint/linted_env'
7
+ require 'env_lint/variable'
8
+ require 'env_lint/version'
9
+
10
+ module EnvLint
11
+ def self.verify_hash(env_definition_file, env)
12
+ DotEnvFile.from_file(env_definition_file).verify_no_missing(env.keys)
13
+ end
14
+
15
+ def self.verify_export_output(env_definition_file, export_output)
16
+ DotEnvFile.from_file(env_definition_file)
17
+ .verify_no_missing(EnvKeyParser.new.parse_export_output(export_output))
18
+ end
19
+
20
+ def self.verify_args(env_definition_file, args)
21
+ DotEnvFile.from_file(env_definition_file)
22
+ .verify_no_unknown(EnvKeyParser.new.parse_args(args))
23
+ end
24
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+ require 'env_lint/capistrano'
3
+
4
+ module EnvLint
5
+ describe Capistrano do
6
+ let :config do
7
+ ::Capistrano::Configuration.new
8
+ end
9
+
10
+ let :namespace do
11
+ config.env
12
+ end
13
+
14
+ let :formatter do
15
+ instance_double(Formatter, ok: true, missing_variables: true, unknown_variables: true, error: true)
16
+ end
17
+
18
+ before do
19
+ config.set(:application_user, 'myapp')
20
+
21
+ EnvLint::Capistrano.load_into(config, formatter)
22
+ end
23
+
24
+ describe 'env:lint' do
25
+ it 'calls verify_export_output with result of capute' do
26
+ config.set(:env_definition_file, '.env.example')
27
+
28
+ allow(namespace).to receive(:capture).and_return('declare -x APP=1')
29
+ expect(EnvLint).to receive(:verify_export_output).with('.env.example', 'declare -x APP=1')
30
+
31
+ config.find_and_execute_task('env:lint')
32
+ end
33
+
34
+ it 'passes missing variables to formatter and aborts' do
35
+ dot_env_file = {}
36
+ variables = ['APP']
37
+
38
+ allow(namespace).to receive(:capture).and_return('declare -x APP=1')
39
+ allow(EnvLint).to receive(:verify_export_output).and_raise(MissingVariables.new(dot_env_file, variables))
40
+ expect(namespace).to receive(:abort)
41
+ expect(formatter).to receive(:missing_variables).with(dot_env_file, variables)
42
+
43
+ config.find_and_execute_task('env:lint')
44
+ end
45
+
46
+ it 'passes error message to formatter and aborts' do
47
+ dot_env_file = {}
48
+ variables = ['APP']
49
+
50
+ allow(namespace).to receive(:capture).and_return('declare -x APP=1')
51
+ allow(EnvLint).to receive(:verify_export_output).and_raise(Error.new('message'))
52
+ expect(namespace).to receive(:abort)
53
+ expect(formatter).to receive(:error).with('message')
54
+
55
+ config.find_and_execute_task('env:lint')
56
+ end
57
+ end
58
+
59
+ describe 'env:lint_args' do
60
+ it 'calls verify_args with result of capute' do
61
+ config.set(:env_definition_file, '.env.example')
62
+
63
+ allow(namespace).to receive(:env_args).and_return(['APP=1'])
64
+ expect(EnvLint).to receive(:verify_args).with('.env.example', ['APP=1'])
65
+
66
+ config.find_and_execute_task('env:lint_args')
67
+ end
68
+
69
+ it 'passes missing variables to formatter and aborts' do
70
+ dot_env_file = {}
71
+ variables = ['APP']
72
+
73
+ allow(namespace).to receive(:env_args).and_return(['APP=1'])
74
+ allow(EnvLint).to receive(:verify_args).and_raise(UnknownVariables.new(dot_env_file, variables))
75
+ expect(namespace).to receive(:abort)
76
+ expect(formatter).to receive(:unknown_variables).with(dot_env_file, variables)
77
+
78
+ config.find_and_execute_task('env:lint_args')
79
+ end
80
+
81
+ it 'passes error message to formatter and aborts' do
82
+ dot_env_file = {}
83
+ variables = ['APP']
84
+
85
+ allow(namespace).to receive(:env_args).and_return(['APP=1'])
86
+ allow(EnvLint).to receive(:verify_args).and_raise(Error.new('message'))
87
+ expect(namespace).to receive(:abort)
88
+ expect(formatter).to receive(:error).with('message')
89
+
90
+ config.find_and_execute_task('env:lint_args')
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,73 @@
1
+ require 'spec_helper'
2
+
3
+ module EnvLint
4
+ describe DotEnvFile do
5
+ describe '#verify_no_missing' do
6
+ it 'passes if all non optional variables are present in the env' do
7
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP')])
8
+
9
+ expect {
10
+ dot_env_file.verify_no_missing(['APP'])
11
+ }.not_to raise_error
12
+ end
13
+
14
+ it 'raises an exception listing undefined non optional variables' do
15
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP'), Variable.new('URL')])
16
+
17
+ expect {
18
+ dot_env_file.verify_no_missing(['APP'])
19
+ }.to raise_error(MissingVariables)
20
+ end
21
+
22
+ it 'lists undefined non optional variables in exception' do
23
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP'), Variable.new('URL'), Variable.new('OTHER')])
24
+
25
+ begin
26
+ dot_env_file.verify_no_missing(['APP'])
27
+ rescue MissingVariables => e
28
+ expect(e.missing_variables.size).to eq(2)
29
+ expect(e.missing_variables.first.name).to eq('URL')
30
+ expect(e.missing_variables.last.name).to eq('OTHER')
31
+ end
32
+ end
33
+
34
+ it 'passes if optional variable is not defined' do
35
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP', '', true)])
36
+
37
+ expect {
38
+ dot_env_file.verify_no_missing([''])
39
+ }.not_to raise_error
40
+ end
41
+ end
42
+
43
+ describe '#verfy_no_unknown' do
44
+ it 'passes if all variables of the env are known' do
45
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP')])
46
+
47
+ expect {
48
+ dot_env_file.verify_no_unknown(['APP'])
49
+ }.not_to raise_error
50
+ end
51
+
52
+ it 'raises an exception if a variable is unknown' do
53
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP')])
54
+
55
+ expect {
56
+ dot_env_file.verify_no_unknown(['APP', 'UNKNOWN', 'OTHER'])
57
+ }.to raise_error(UnknownVariables)
58
+ end
59
+
60
+ it 'lists unknown variables in exception' do
61
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP')])
62
+
63
+ begin
64
+ dot_env_file.verify_no_unknown(['APP', 'UNKNOWN', 'OTHER'])
65
+ rescue UnknownVariables => e
66
+ expect(e.unknown_variables.size).to eq(2)
67
+ expect(e.unknown_variables.first).to eq('UNKNOWN')
68
+ expect(e.unknown_variables.last).to eq('OTHER')
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ module EnvLint
4
+ describe DotEnvParser do
5
+ describe '#parse' do
6
+ it 'recognizes variables' do
7
+ variables = DotEnvParser.new.parse(<<-END)
8
+ APP=myapp
9
+ END
10
+
11
+ expect(variables.first.name).to eq('APP')
12
+ expect(variables.first.value).to eq('myapp')
13
+ end
14
+
15
+ it 'recognizes double quoted values' do
16
+ variables = DotEnvParser.new.parse(<<-END)
17
+ APP="my app"
18
+ END
19
+
20
+ expect(variables.first.value).to eq('my app')
21
+ end
22
+
23
+ it 'recognizes single quoted values' do
24
+ variables = DotEnvParser.new.parse(<<-END)
25
+ APP='my app'
26
+ END
27
+
28
+ expect(variables.first.value).to eq('my app')
29
+ end
30
+
31
+ it 'handles empty assignments' do
32
+ variables = DotEnvParser.new.parse(<<-END)
33
+ APP=
34
+ END
35
+
36
+ expect(variables.first.value).to eq('')
37
+ end
38
+
39
+ it 'ignores blank linkes' do
40
+ variables = DotEnvParser.new.parse(<<-END)
41
+ APP1=myapp
42
+
43
+ APP2=myapp
44
+ END
45
+
46
+ expect(variables.count).to eq(2)
47
+ expect(variables.first.name).to eq('APP1')
48
+ expect(variables.last.name).to eq('APP2')
49
+ end
50
+
51
+ it 'recognizes optional variables' do
52
+ variables = DotEnvParser.new.parse(<<-END)
53
+ # APP=myapp
54
+ #OTHER=myapp
55
+ APP=myapp
56
+ END
57
+
58
+ expect(variables[0]).to be_optional
59
+ expect(variables[1]).to be_optional
60
+ expect(variables[2]).not_to be_optional
61
+ end
62
+
63
+ it 'recognizes comments' do
64
+ variables = DotEnvParser.new.parse(<<-END)
65
+ # The name of the app
66
+ APP=myapp
67
+ END
68
+
69
+ expect(variables.first.comment).to eq('The name of the app')
70
+ end
71
+
72
+ it 'recognizes multi line comments' do
73
+ variables = DotEnvParser.new.parse(<<-END)
74
+ # The name of the app
75
+ # and here the text goes on
76
+ APP=myapp
77
+ END
78
+
79
+ expect(variables.first.comment).to eq("The name of the app\nand here the text goes on")
80
+ end
81
+
82
+ it 'starts new comment at blank line' do
83
+ variables = DotEnvParser.new.parse(<<-END)
84
+ # This is some header which is not related
85
+ # to the following variable
86
+
87
+ # This is the comment
88
+ APP=myapp
89
+ END
90
+
91
+ expect(variables.first.comment).to eq('This is the comment')
92
+ end
93
+
94
+ it 'raises exception for unrecognized line' do
95
+ text = <<-END
96
+ what is this?
97
+ END
98
+
99
+ expect {
100
+ DotEnvParser.new.parse(text)
101
+ }.to raise_error(UnrecognizedDotEnvLine)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ module EnvLint
4
+ describe EnvKeyParser do
5
+ describe '.parse_args' do
6
+ it 'recognizes assignments' do
7
+ parser = EnvKeyParser.new
8
+
9
+ keys = parser.parse_args(['APP=myname', 'NAME=test'])
10
+
11
+ expect(keys).to eq(['APP', 'NAME'])
12
+ end
13
+
14
+ it 'ignores other args' do
15
+ parser = EnvKeyParser.new
16
+
17
+ keys = parser.parse_args(['env:set', 'APP=myname'])
18
+
19
+ expect(keys).to eq(['APP'])
20
+ end
21
+ end
22
+
23
+ describe '.parse_export_output' do
24
+ it 'recognizes assignments' do
25
+ parser = EnvKeyParser.new
26
+
27
+ keys = parser.parse_export_output(<<-END)
28
+ declare -x APP="myapp"
29
+ declare -x NAME="test"
30
+ END
31
+
32
+ expect(keys).to eq(['APP', 'NAME'])
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,85 @@
1
+ require 'spec_helper'
2
+
3
+ module EnvLint
4
+ describe LintedEnv do
5
+ describe '#fetch' do
6
+ it 'returns value from env for known variable' do
7
+ env = {'APP' => 'myapp'}
8
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP')])
9
+ linted_env = LintedEnv.new(env, dot_env_file)
10
+
11
+ expect(linted_env.fetch('APP')).to eq('myapp')
12
+ end
13
+
14
+ it 'translates symbols to uppercase strings' do
15
+ env = {'APP_NAME' => 'myapp'}
16
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP_NAME')])
17
+ linted_env = LintedEnv.new(env, dot_env_file)
18
+
19
+ expect(linted_env.fetch(:app_name)).to eq('myapp')
20
+ end
21
+
22
+ it 'raises exception for unknown variable' do
23
+ env = {'APP' => 'myapp'}
24
+ dot_env_file = DotEnvFile.new('.env.example', [])
25
+ linted_env = LintedEnv.new(env, dot_env_file)
26
+
27
+ expect {
28
+ linted_env.fetch('APP')
29
+ }.to raise_error(UnknownVariable)
30
+ end
31
+
32
+ it 'raises exception for undefined variable' do
33
+ env = {}
34
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP')])
35
+ linted_env = LintedEnv.new(env, dot_env_file)
36
+
37
+ expect {
38
+ linted_env.fetch('APP')
39
+ }.to raise_error(MissingVariable)
40
+ end
41
+
42
+ it 'returns default value for undefined variable' do
43
+ env = {}
44
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP')])
45
+ linted_env = LintedEnv.new(env, dot_env_file)
46
+
47
+ expect(linted_env.fetch('APP', 'default')).to eq('default')
48
+ end
49
+
50
+ it 'returns default from block for undefined variable' do
51
+ env = {}
52
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP')])
53
+ linted_env = LintedEnv.new(env, dot_env_file)
54
+
55
+ expect(linted_env.fetch('APP') { 'default' }).to eq('default')
56
+ end
57
+
58
+ it 'raises exception if optional variable is used without default' do
59
+ env = {'APP' => 'myapp'}
60
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP', '', true)])
61
+ linted_env = LintedEnv.new(env, dot_env_file)
62
+
63
+ expect {
64
+ linted_env.fetch('APP')
65
+ }.to raise_error(DefaultValueRequiredForOptionalVariable)
66
+ end
67
+
68
+ it 'allows to use optional variable with default value' do
69
+ env = {'APP' => 'myapp'}
70
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP', '', true)])
71
+ linted_env = LintedEnv.new(env, dot_env_file)
72
+
73
+ expect(linted_env.fetch('APP', 'default')).to eq('myapp')
74
+ end
75
+
76
+ it 'allows to use optional variable with block' do
77
+ env = {'APP' => 'myapp'}
78
+ dot_env_file = DotEnvFile.new('.env.example', [Variable.new('APP', '', true)])
79
+ linted_env = LintedEnv.new(env, dot_env_file)
80
+
81
+ expect(linted_env.fetch('APP') { 'block' }).to eq('myapp')
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+ require 'fileutils'
3
+
4
+ describe EnvLint do
5
+ SANDBOX_DIR = File.join(File.dirname(__FILE__), 'tmp')
6
+
7
+ around do |example|
8
+ clean_up_sandbox
9
+ in_sandbox { example.run }
10
+ end
11
+
12
+ describe '.verify_hash' do
13
+ it 'passes when hash is complete' do
14
+ File.write('.env.example', <<-END)
15
+ APP=myapp
16
+ END
17
+
18
+ expect {
19
+ EnvLint.verify_hash('.env.example', {'APP' => 'some name'})
20
+ }.not_to raise_error
21
+ end
22
+
23
+ it 'fails when hash is missing non optional variable' do
24
+ File.write('.env.example', <<-END)
25
+ APP=myapp
26
+ END
27
+
28
+ expect {
29
+ EnvLint.verify_hash('.env.example', {'OTHER' => 'other'})
30
+ }.to raise_error(EnvLint::MissingVariables)
31
+ end
32
+ end
33
+
34
+ describe '.verify_args' do
35
+ it 'passes when all args are known' do
36
+ File.write('.env.example', <<-END)
37
+ APP=myapp
38
+ OTHER=other
39
+ END
40
+
41
+ expect {
42
+ EnvLint.verify_args('.env.example', ['APP=name'])
43
+ }.not_to raise_error
44
+ end
45
+
46
+ it 'fails when there are unknown args' do
47
+ File.write('.env.example', <<-END)
48
+ APP=myapp
49
+ END
50
+
51
+ expect {
52
+ EnvLint.verify_args('.env.example', ['OTHER=name'])
53
+ }.to raise_error(EnvLint::UnknownVariables)
54
+ end
55
+ end
56
+
57
+ describe '.verify_export_output' do
58
+ it 'passes when the env is complete' do
59
+ File.write('.env.example', <<-END)
60
+ APP=myapp
61
+ END
62
+ export_output = <<-END
63
+ declare -x APP="myapp"
64
+ declare -x OTHER="other"
65
+ END
66
+
67
+ expect {
68
+ EnvLint.verify_export_output('.env.example', export_output)
69
+ }.not_to raise_error
70
+ end
71
+
72
+ it 'fails when the env is missing non optional variable' do
73
+ File.write('.env.example', <<-END)
74
+ APP=myapp
75
+ END
76
+ export_output = <<-END
77
+ declare -x OTHER="other"
78
+ END
79
+
80
+ expect {
81
+ EnvLint.verify_export_output('.env.example', export_output)
82
+ }.to raise_error(EnvLint::MissingVariables)
83
+ end
84
+ end
85
+
86
+ describe EnvLint::LintedEnv do
87
+ it 'allows to read defined variables from ENV' do
88
+ File.write('.env.example', <<-END)
89
+ APP=myapp
90
+ END
91
+ allow(ENV).to receive(:fetch).with('APP').and_return('value')
92
+
93
+ linted_env = EnvLint::LintedEnv.from_file('.env.example')
94
+
95
+ expect(linted_env.fetch(:app)).to eq('value')
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def clean_up_sandbox
102
+ FileUtils.rm_rf(SANDBOX_DIR)
103
+ FileUtils.mkdir_p(SANDBOX_DIR)
104
+ end
105
+
106
+ def in_sandbox(&block)
107
+ Dir.chdir(SANDBOX_DIR, &block)
108
+ end
109
+ end
@@ -0,0 +1,5 @@
1
+ require 'rspec'
2
+
3
+ require 'env_lint'
4
+
5
+ Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |f| require f }
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: env_lint
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Tim Fischbach
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: formatador
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: capistrano
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '2.9'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '2.9'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 3.0.0.beta1
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 3.0.0.beta1
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.3'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email:
85
+ - mail@timfischbach.de
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - .gitignore
91
+ - .travis.yml
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - env_lint.gemspec
97
+ - lib/env_lint.rb
98
+ - lib/env_lint/capistrano.rb
99
+ - lib/env_lint/dot_env_file.rb
100
+ - lib/env_lint/dot_env_parser.rb
101
+ - lib/env_lint/env_key_parser.rb
102
+ - lib/env_lint/errors.rb
103
+ - lib/env_lint/formatter.rb
104
+ - lib/env_lint/linted_env.rb
105
+ - lib/env_lint/tasks.rb
106
+ - lib/env_lint/variable.rb
107
+ - lib/env_lint/version.rb
108
+ - spec/env_lint/capistrano_spec.rb
109
+ - spec/env_lint/dot_env_file_spec.rb
110
+ - spec/env_lint/dot_env_parser_spec.rb
111
+ - spec/env_lint/env_key_parser_spec.rb
112
+ - spec/env_lint/linted_env_spec.rb
113
+ - spec/integration_spec.rb
114
+ - spec/spec_helper.rb
115
+ homepage: ''
116
+ licenses:
117
+ - MIT
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - '>='
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - '>='
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubyforge_project:
135
+ rubygems_version: 2.2.2
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: Lint the environment according by .env.example
139
+ test_files:
140
+ - spec/env_lint/capistrano_spec.rb
141
+ - spec/env_lint/dot_env_file_spec.rb
142
+ - spec/env_lint/dot_env_parser_spec.rb
143
+ - spec/env_lint/env_key_parser_spec.rb
144
+ - spec/env_lint/linted_env_spec.rb
145
+ - spec/integration_spec.rb
146
+ - spec/spec_helper.rb