nvar 0.1.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.
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # `Nvar`
2
+
3
+ If your app relies on lots of environment secrets, onboarding's tough. New team members need to add credentials for fifty different services and configure no end of app settings through their environment. Worse still, if a merged PR introduces something new, it can lead to inconvenient and unpredictable errors. **`Nvar` helps keep your team in step** by making sure all necessary environment variables are set.
4
+
5
+ You can use `Nvar` in Ruby apps, with out-of-the-box support provided for Rails.
6
+ ## Installation
7
+
8
+ Add the gem to your Gemfile and install it with `bundle add nvar`. If you're not on Rails, you'll need to make sure that `Nvar` is required with `require 'nvar'`, and then manually call `Nvar::EnvironmentVariable.load_all` as early as is appropriate.
9
+ ## Configuration
10
+
11
+ `Nvar` is configured by way of `config/environment_variables.yml`. If you're on Rails, this file will be created for you automatically. Each key corresponds to the name of a required environment variable, and houses its configuration, all of which is optional.
12
+
13
+ ```yml
14
+ REQUIRED_ENV_VAR:
15
+ required: false # defaults to true
16
+ type: Integer # defaults to String
17
+ default_value: 8
18
+ filter_from_requests: true # defaults to false
19
+ passthrough: true # defaults to false
20
+ ```
21
+
22
+ - **required** determines whether an error will be raised if the environment variable is unset during initialization.
23
+ - **type** determines which type the variable will be cast to on load.
24
+ - **default_value** decides the value of the environment variable if it's absent.
25
+ - **filter_from_requests** is potentially the most exciting of the bunch - it integrates with the `vcr` gem. If your environment variable is a secret that's used directly (e.g. in bearer token authentication), use `true`. If it's used on its own as the password for basic auth, use `alone_with_basic_auth_password`. To activate the filtering, configure `VCR` as follows:
26
+
27
+ ```ruby
28
+ VCR.configure do |config|
29
+ Nvar::EnvironmentVariable.filter_from_vcr_cassettes(config)
30
+ end
31
+ ```
32
+
33
+ Now, if you use `VCR` to record a request, that credential will be hidden using `vcr`'s `#filter_sensitive_data` hooks.
34
+
35
+ This is just a glimpse of `Nvar`'s greater aim - centralizing configuration for your environment variables as much as possible. Doing so enables you to onboard developers easily, and makes it easier to hide environment variables from logs and files.
36
+
37
+ ### Passthrough
38
+
39
+ The final config option, `passthrough`, deserves some extra detail. By default, `Nvar` sets your environment constants to their actual values in development and production environments, and to their names in test environments.
40
+
41
+ In production/development, or in test with passthrough active:
42
+
43
+ ```
44
+ irb(main):001:0> REQUIRED_ENV_VAR
45
+ => "set"
46
+ ```
47
+
48
+ In test:
49
+
50
+ ```
51
+ irb(main):001:0> REQUIRED_ENV_VAR
52
+ => "REQUIRED_ENV_VAR"
53
+ ```
54
+
55
+ Your tests shouldn't be reliant on your environment, so generally, you want to have `passthrough` set to `true` as little as possible. What it *is* useful for, however, is recording VCR cassettes. Set `passthrough: true` on necessary environment variables before recording VCR cassettes, then remove it and run your tests again to make sure they're not reliant on your environment.
56
+
57
+
58
+ ## Usage
59
+
60
+ Now that you've been through and configured the environment variables that are necessary for your app, `Nvar` will write your environment variables to top-level constants, cast to any types you've specified, and raise an informative error if any are absent.
61
+ ### .env files
62
+
63
+ `Nvar` works well with gems like `dotenv` that source their config from a `.env` file. If an environment variable is unset when the app initializes and isn't present in `.env`, it will be added to that file. If a default value is specified in your `Nvar` config, that will be passed to `.env` too.
64
+
65
+ When using gems such as `dotenv`, make sure you load those first so that the environment is ready for `Nvar` to check.
66
+
67
+ ## Development
68
+
69
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
70
+
71
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
72
+
73
+ ## Contributing
74
+
75
+ Bug reports and pull requests are welcome on GitHub at https://github.com/boardfish/nvar. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/boardfish/nvar/blob/master/CODE_OF_CONDUCT.md).
76
+
77
+
78
+ ## License
79
+
80
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
81
+
82
+ ## Code of Conduct
83
+
84
+ Everyone interacting in the Nvar project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/boardfish/nvar/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/brakeman ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'brakeman' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("brakeman", "brakeman")
data/bin/bundle-audit ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'bundle-audit' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("bundler-audit", "bundle-audit")
data/bin/bundler-audit ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'bundler-audit' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("bundler-audit", "bundler-audit")
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'nvar'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/rspec ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rspec-core", "rspec")
data/bin/rubocop ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require "pathname"
12
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ bundle_binstub = File.expand_path("../bundle", __FILE__)
16
+
17
+ if File.file?(bundle_binstub)
18
+ if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
19
+ load(bundle_binstub)
20
+ else
21
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
22
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
23
+ end
24
+ end
25
+
26
+ require "rubygems"
27
+ require "bundler/setup"
28
+
29
+ load Gem.bin_path("rubocop", "rubocop")
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+ require_relative './environment_variable'
5
+
6
+ module Nvar
7
+ class Engine < Rails::Engine # :nodoc:
8
+ # Load environment variables from `config/environment_variables.yml`, and assign
9
+ # them to constants in the app. The EnvironmentVariable class will raise an
10
+ # error if it can't source a required env var from the environment, and set
11
+ # values for use during tests.
12
+ config.after_initialize do |app|
13
+ Nvar::EnvironmentVariable.configure_for_rails(app)
14
+ Nvar::EnvironmentVariable.load_all
15
+ rescue Nvar::EnvironmentVariableNotPresentError => e
16
+ raise e unless Rails.env.test?
17
+
18
+ e.vars.each do |var|
19
+ Object.const_set(var.name, var.name)
20
+ end
21
+ end
22
+
23
+ rake_tasks do
24
+ load 'nvar/rails/tasks/verify_environment_file.rake'
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Wrapper for retrieval of environment variables. See
4
+ # config/initializers/environment_variable_loader.rb to check out how it's used.
5
+ require 'active_support/core_ext/hash/keys'
6
+ require 'active_support/core_ext/object/blank'
7
+ require 'yaml'
8
+
9
+ module Nvar
10
+ # Wrapper for loading environment variables, used across relevant Rake tasks
11
+ class EnvironmentVariable
12
+ attr_reader :name, :type, :value, :required, :defined
13
+
14
+ def initialize(name:, type: 'String', filter_from_requests: nil, **args)
15
+ @name = name
16
+ @type = type
17
+ @required = args[:required].nil? ? true : args[:required]
18
+ @filter_from_requests = filter_from_requests.yield_self { |f| [true, false].include?(f) ? f : f&.to_sym }
19
+ @value = fetch_value(args.slice(:passthrough, :default_value))
20
+ @defined = true
21
+ rescue KeyError
22
+ @value = args[:default_value]
23
+ @defined = false
24
+ end
25
+
26
+ def to_const
27
+ raise Nvar::EnvironmentVariableNotPresentError, self unless defined
28
+
29
+ Object.const_set(name, typecast_value)
30
+ end
31
+
32
+ def set?
33
+ return false unless defined
34
+
35
+ return value.present? if required
36
+
37
+ true
38
+ end
39
+
40
+ def add_to_env_file
41
+ return if present_in_env_file?
42
+
43
+ File.write(Nvar.env_file_path, to_env_assign, mode: 'a')
44
+ end
45
+
46
+ def filter_from_vcr_cassettes(config)
47
+ return if @filter_from_requests.nil? || !@filter_from_requests
48
+
49
+ config.filter_sensitive_data("<#{name}>") do
50
+ # :nocov:
51
+ case @filter_from_requests
52
+ when :alone_as_basic_auth_password
53
+ Base64.encode64(['', @value].join(':')).delete("\n")
54
+ when true
55
+ @value
56
+ end
57
+ # :nocov:
58
+ end
59
+ config
60
+ end
61
+
62
+ private
63
+
64
+ def present_in_env_file?
65
+ File.open(Nvar.env_file_path) { |f| f.each_line.find { |line| line.start_with?("#{name}=") } }
66
+ end
67
+
68
+ def to_env_assign
69
+ "#{name}=#{value}\n"
70
+ end
71
+
72
+ def typecast_value
73
+ return value if value.nil?
74
+
75
+ Kernel.public_send(type.to_sym, value)
76
+ end
77
+
78
+ def fetch_value(passthrough: false, default_value: nil)
79
+ return (default_value || name) if ENV['RAILS_ENV'] == 'test' && !passthrough
80
+
81
+ required ? ENV.fetch(name.to_s) : ENV[name.to_s]
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :nvar do
4
+ task :verify_environment_file do
5
+ Nvar::EnvironmentVariable.configure_for_rails(Rails)
6
+ Nvar::EnvironmentVariable.verify_env ? exit : exit(1)
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nvar
4
+ VERSION = '0.1.0'
5
+ end
data/lib/nvar.rb ADDED
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nvar/version'
4
+ require 'nvar/environment_variable'
5
+ require 'nvar/engine' if defined?(Rails)
6
+ require 'active_support/core_ext/module/attribute_accessors'
7
+
8
+ # Centralized configuration for required environment variables in your Ruby app.
9
+ module Nvar
10
+ mattr_accessor :config_file_path, default: File.expand_path('config/environment_variables.yml')
11
+ mattr_accessor :env_file_path, default: File.expand_path('.env')
12
+
13
+ # Comments in .env files must have a leading '#' symbol. This cannot be
14
+ # followed by a space.
15
+ ENV_COMMENT = <<~'COMMENT'
16
+ #Environment variables are managed through this file (.env). The Scripts to
17
+ #Rule Them All (in script/) load the environment from here, and the app warns
18
+ #on startup if any required environment variables are missing. You can see the
19
+ #list of environment variables that can be set for the app in
20
+ #config/environment_variables.yml.
21
+ COMMENT
22
+
23
+ class Error < StandardError; end
24
+
25
+ # Error that is raised when an environment variable is blank or unset when it is
26
+ # required
27
+ class EnvironmentVariableNotPresentError < Error
28
+ attr_reader :vars
29
+
30
+ def initialize(*vars)
31
+ @vars = vars
32
+ super()
33
+ end
34
+
35
+ def message
36
+ "The following variables are unset or blank: #{vars.map(&:name).join(', ')}"
37
+ end
38
+ end
39
+
40
+ class << self
41
+ def configure_for_rails(app)
42
+ self.config_file_path = app.root.join('config/environment_variables.yml')
43
+ self.env_file_path = app.root.join('.env')
44
+ [config_file_path, env_file_path].each do |path|
45
+ File.open(path, 'w') {} unless path.exist? # rubocop:disable Lint/EmptyBlock
46
+ end
47
+ end
48
+
49
+ def load_all
50
+ all.tap do |set, unset|
51
+ set.map(&:to_const)
52
+ raise EnvironmentVariableNotPresentError.new(*unset) if unset.any?
53
+ end
54
+ end
55
+
56
+ def filter_from_vcr_cassettes(config)
57
+ set, = all
58
+ set.reduce(config) do |c, env_var|
59
+ c.tap { env_var.filter_from_vcr_cassettes(c) }
60
+ end
61
+ end
62
+
63
+ def all
64
+ variables.map do |variable_name, config|
65
+ EnvironmentVariable.new(**(config || {}).merge(name: variable_name))
66
+ end.partition(&:set?)
67
+ end
68
+
69
+ def touch_env
70
+ File.write(env_file_path, ENV_COMMENT, mode: 'w') unless File.exist?(env_file_path)
71
+ end
72
+
73
+ def verify_env(write_to_file: true)
74
+ _set, unset = all
75
+ return true if all_required_env_variables_set?
76
+
77
+ puts 'Please update .env with values for each environment variable:'
78
+ touch_env if write_to_file
79
+ unset.each do |variable|
80
+ variable.add_to_env_file if write_to_file
81
+ puts "- #{variable.name}"
82
+ end
83
+ puts "#{config_file_path} contains information on required environment variables across the app."
84
+ # Don't exit if all unset variables had defaults that were written to .env
85
+ write_to_file && unset.all? { |variable| variable.value.present? }
86
+ end
87
+
88
+ private
89
+
90
+ def all_required_env_variables_set?
91
+ all[1].none? || ENV['RAILS_ENV'] == 'test'
92
+ end
93
+
94
+ def variables
95
+ (YAML.safe_load(File.read(config_file_path)) || {}).deep_symbolize_keys
96
+ end
97
+ end
98
+ end
data/nvar.gemspec ADDED
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/nvar/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'nvar'
7
+ spec.version = Nvar::VERSION
8
+ spec.authors = ['Simon Fish']
9
+ spec.email = ['si@mon.fish']
10
+
11
+ spec.summary = 'Manage environment variables in Ruby'
12
+ spec.description = 'Manage environment variables in Ruby'
13
+ spec.homepage = 'https://github.com/boardfish/nvar'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
16
+
17
+ spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = spec.homepage
21
+ spec.metadata['changelog_uri'] = spec.homepage
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ end
28
+ spec.bindir = 'exe'
29
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30
+ spec.require_paths = ['lib']
31
+ spec.add_runtime_dependency 'activesupport', ['>= 5.0.0', '< 8.0']
32
+ spec.add_development_dependency 'bundler-audit'
33
+ spec.add_development_dependency 'byebug'
34
+ spec.add_development_dependency 'climate_control'
35
+ spec.add_development_dependency 'rails'
36
+ spec.add_development_dependency 'rubocop', '~> 1.12.0'
37
+ spec.add_development_dependency 'rubocop-rake'
38
+ spec.add_development_dependency 'rubocop-rspec'
39
+ spec.add_development_dependency 'simplecov'
40
+ spec.add_development_dependency 'tempfile'
41
+ spec.add_development_dependency 'vcr'
42
+ spec.metadata = {
43
+ 'rubygems_mfa_required' => 'true'
44
+ }
45
+ end
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ # Exit if repo already exists
3
+ [ -d "replicate-bug" ] && echo "Sample repo exists. Rename or remove it to begin." && exit
4
+ branch_name=$(git rev-parse --abbrev-ref HEAD)
5
+ # Ensure that when we install nvar in the repo, we install this copy.
6
+ bundle config local.nvar $(pwd)
7
+ # Create and enter a minimal example repo
8
+ rails new --minimal replicate-bug
9
+ cd replicate-bug
10
+ # Add our local copy of ViewComponent
11
+ bundle add nvar --git https://github.com/boardfish/nvar --branch $branch_name
12
+ # Generate a controller
13
+ rails g controller Home index
14
+ # Root to the index action on HomeController
15
+ cat << 'ROUTES' > 'config/routes.rb'
16
+ Rails.application.routes.draw do
17
+ root to: 'home#index'
18
+ end
19
+ ROUTES