├── .prettierignore
├── test
├── support
│ └── rg.rb
├── test_helper.rb
├── configurable_from_env
│ ├── version_test.rb
│ └── environment_value_test.rb
└── configurable_from_env_test.rb
├── lib
├── configurable_from_env
│ ├── version.rb
│ ├── environment_value.rb
│ └── configurable.rb
└── configurable_from_env.rb
├── CHANGELOG.md
├── .gitignore
├── bin
├── setup
└── console
├── Appraisals
├── Gemfile
├── .github
├── workflows
│ ├── release-drafter.yml
│ └── ci.yml
├── dependabot.yml
└── release-drafter.yml
├── gemfiles
├── rails_7.2.gemfile
├── rails_8.0.gemfile
└── rails_main.gemfile
├── .kodiak.toml
├── .overcommit.yml
├── LICENSE.txt
├── configurable_from_env.gemspec
├── .rubocop.yml
├── Rakefile
├── CODE_OF_CONDUCT.md
└── README.md
/.prettierignore:
--------------------------------------------------------------------------------
1 | /CODE_OF_CONDUCT.md
2 |
--------------------------------------------------------------------------------
/test/support/rg.rb:
--------------------------------------------------------------------------------
1 | # Enable color test output
2 | require "minitest/rg"
3 |
--------------------------------------------------------------------------------
/lib/configurable_from_env/version.rb:
--------------------------------------------------------------------------------
1 | module ConfigurableFromEnv
2 | VERSION = "0.3.0".freeze
3 | end
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Release notes for this project are kept here: https://github.com/mattbrictson/configurable_from_env/releases
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /site/
8 | /spec/reports/
9 | /tmp/
10 | /Gemfile.lock
11 | /gemfiles/*.lock
12 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
2 | require "configurable_from_env"
3 |
4 | require "minitest/autorun"
5 | Dir[File.expand_path("support/**/*.rb", __dir__)].each { |rb| require(rb) }
6 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | which overcommit > /dev/null 2>&1 && overcommit --install
7 | bundle install
8 |
9 | # Do any other automated setup that you need to do here
10 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise "rails-7.2" do
2 | gem "activesupport", "~> 7.2.2"
3 | end
4 |
5 | appraise "rails-8.0" do
6 | gem "activesupport", "~> 8.0.0"
7 | end
8 |
9 | appraise "rails-main" do
10 | gem "activesupport", github: "rails/rails", branch: "main"
11 | end
12 |
--------------------------------------------------------------------------------
/test/configurable_from_env/version_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | module ConfigurableFromEnv
4 | class VersionTest < Minitest::Test
5 | def test_that_it_has_a_version_number
6 | refute_nil ::ConfigurableFromEnv::VERSION
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 | gemspec
3 |
4 | gem "appraisal"
5 | gem "irb"
6 | gem "minitest", "~> 5.11"
7 | gem "minitest-rg", "~> 5.3"
8 | gem "rake", "~> 13.0"
9 | gem "rubocop", "1.81.7"
10 | gem "rubocop-minitest", "0.38.2"
11 | gem "rubocop-packaging", "0.6.0"
12 | gem "rubocop-performance", "1.26.1"
13 | gem "rubocop-rake", "0.7.1"
14 |
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | permissions:
9 | contents: write
10 | pull-requests: read
11 |
12 | jobs:
13 | update_release_draft:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: release-drafter/release-drafter@v6
17 | env:
18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "bundler/setup"
4 | require "configurable_from_env"
5 |
6 | # You can add fixtures and/or initialization code here to make experimenting
7 | # with your gem easier. You can also use a different console, if you like.
8 |
9 | # (If you use this, don't forget to add pry to your Gemfile!)
10 | # require "pry"
11 | # Pry.start
12 |
13 | require "irb"
14 | IRB.start(__FILE__)
15 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.2.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "minitest", "~> 5.11"
7 | gem "minitest-rg", "~> 5.3"
8 | gem "rake", "~> 13.0"
9 | gem "rubocop", "1.68.0"
10 | gem "rubocop-minitest", "0.36.0"
11 | gem "rubocop-packaging", "0.5.2"
12 | gem "rubocop-performance", "1.22.1"
13 | gem "rubocop-rake", "0.6.0"
14 | gem "activesupport", "~> 7.2.2"
15 |
16 | gemspec path: "../"
17 |
--------------------------------------------------------------------------------
/gemfiles/rails_8.0.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "minitest", "~> 5.11"
7 | gem "minitest-rg", "~> 5.3"
8 | gem "rake", "~> 13.0"
9 | gem "rubocop", "1.68.0"
10 | gem "rubocop-minitest", "0.36.0"
11 | gem "rubocop-packaging", "0.5.2"
12 | gem "rubocop-performance", "1.22.1"
13 | gem "rubocop-rake", "0.6.0"
14 | gem "activesupport", "~> 8.0.0"
15 |
16 | gemspec path: "../"
17 |
--------------------------------------------------------------------------------
/gemfiles/rails_main.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "appraisal"
6 | gem "minitest", "~> 5.11"
7 | gem "minitest-rg", "~> 5.3"
8 | gem "rake", "~> 13.0"
9 | gem "rubocop", "1.68.0"
10 | gem "rubocop-minitest", "0.36.0"
11 | gem "rubocop-packaging", "0.5.2"
12 | gem "rubocop-performance", "1.22.1"
13 | gem "rubocop-rake", "0.6.0"
14 | gem "activesupport", github: "rails/rails", branch: "main"
15 |
16 | gemspec path: "../"
17 |
--------------------------------------------------------------------------------
/.kodiak.toml:
--------------------------------------------------------------------------------
1 | # .kodiak.toml
2 | # Minimal config. version is the only required field.
3 | version = 1
4 |
5 | [merge.automerge_dependencies]
6 | # auto merge all PRs opened by "dependabot" that are "minor" or "patch" version upgrades. "major" version upgrades will be ignored.
7 | versions = ["minor", "patch"]
8 | usernames = ["dependabot"]
9 |
10 | # if using `update.always`, add dependabot to `update.ignore_usernames` to allow
11 | # dependabot to update and close stale dependency upgrades.
12 | [update]
13 | ignored_usernames = ["dependabot"]
14 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: bundler
4 | directory: "/"
5 | schedule:
6 | interval: monthly
7 | time: "16:00"
8 | timezone: America/Los_Angeles
9 | open-pull-requests-limit: 10
10 | labels:
11 | - "🏠 Housekeeping"
12 | - package-ecosystem: github-actions
13 | directory: "/"
14 | schedule:
15 | interval: monthly
16 | time: "16:00"
17 | timezone: America/Los_Angeles
18 | open-pull-requests-limit: 10
19 | labels:
20 | - "🏠 Housekeeping"
21 |
--------------------------------------------------------------------------------
/.overcommit.yml:
--------------------------------------------------------------------------------
1 | # Overcommit hooks run automatically on certain git operations, like "git commit".
2 | # For a complete list of options that you can use to customize hooks, see:
3 | # https://github.com/sds/overcommit
4 |
5 | gemfile: false
6 | verify_signatures: false
7 |
8 | PreCommit:
9 | BundleCheck:
10 | enabled: true
11 |
12 | FixMe:
13 | enabled: true
14 | keywords: ["FIXME"]
15 | exclude:
16 | - .overcommit.yml
17 |
18 | LocalPathsInGemfile:
19 | enabled: true
20 |
21 | RuboCop:
22 | enabled: true
23 | required_executable: bundle
24 | command: ["bundle", "exec", "rubocop"]
25 | on_warn: fail
26 |
27 | YamlSyntax:
28 | enabled: true
29 |
30 | PostCheckout:
31 | ALL:
32 | quiet: true
33 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: "$RESOLVED_VERSION"
2 | tag-template: "v$RESOLVED_VERSION"
3 | categories:
4 | - title: "⚠️ Breaking Changes"
5 | label: "⚠️ Breaking"
6 | - title: "✨ New Features"
7 | label: "✨ Feature"
8 | - title: "🐛 Bug Fixes"
9 | label: "🐛 Bug Fix"
10 | - title: "📚 Documentation"
11 | label: "📚 Docs"
12 | - title: "🏠 Housekeeping"
13 | label: "🏠 Housekeeping"
14 | version-resolver:
15 | minor:
16 | labels:
17 | - "⚠️ Breaking"
18 | - "✨ Feature"
19 | default: patch
20 | change-template: "- $TITLE (#$NUMBER) @$AUTHOR"
21 | no-changes-template: "- No changes"
22 | template: |
23 | $CHANGES
24 |
25 | **Full Changelog:** https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
26 |
--------------------------------------------------------------------------------
/lib/configurable_from_env.rb:
--------------------------------------------------------------------------------
1 | require "active_support"
2 | require "active_support/concern"
3 | require "active_support/core_ext/enumerable"
4 | require "active_support/core_ext/hash/keys"
5 |
6 | module ConfigurableFromEnv
7 | autoload :Configurable, "configurable_from_env/configurable"
8 | autoload :EnvironmentValue, "configurable_from_env/environment_value"
9 | autoload :VERSION, "configurable_from_env/version"
10 |
11 | extend ActiveSupport::Concern
12 | include Configurable
13 |
14 | module ClassMethods
15 | def config_accessor(*attributes, from_env: nil, **options, &)
16 | if from_env && attributes.many?
17 | raise ArgumentError, "Only one accessor at a time can be created using the :from_env option"
18 | end
19 |
20 | env_value = EnvironmentValue.from(from_env)
21 | accessor = super(*attributes, **options, &)
22 | default_provided = options.key?(:default) || block_given?
23 |
24 | env_value&.read(required: !default_provided) do |value|
25 | public_send(:"#{attributes.first}=", value)
26 | end
27 |
28 | accessor
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2025 Matt Brictson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/configurable_from_env.gemspec:
--------------------------------------------------------------------------------
1 | require_relative "lib/configurable_from_env/version"
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = "configurable_from_env"
5 | spec.version = ConfigurableFromEnv::VERSION
6 | spec.authors = ["Matt Brictson"]
7 | spec.email = ["opensource@mattbrictson.com"]
8 |
9 | spec.summary = "Define accessors that are automatically populated via ENV"
10 | spec.homepage = "https://github.com/mattbrictson/configurable_from_env"
11 | spec.license = "MIT"
12 | spec.required_ruby_version = ">= 3.2"
13 |
14 | spec.metadata = {
15 | "bug_tracker_uri" => "https://github.com/mattbrictson/configurable_from_env/issues",
16 | "changelog_uri" => "https://github.com/mattbrictson/configurable_from_env/releases",
17 | "source_code_uri" => "https://github.com/mattbrictson/configurable_from_env",
18 | "homepage_uri" => spec.homepage,
19 | "rubygems_mfa_required" => "true"
20 | }
21 |
22 | # Specify which files should be added to the gem when it is released.
23 | spec.files = Dir.glob(%w[LICENSE.txt README.md {exe,lib}/**/*]).reject { |f| File.directory?(f) }
24 | spec.bindir = "exe"
25 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26 | spec.require_paths = ["lib"]
27 |
28 | # Runtime dependencies
29 | spec.add_dependency "activesupport", ">= 7.2"
30 | end
31 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | pull_request:
4 | push:
5 | branches:
6 | - main
7 | jobs:
8 | rubocop:
9 | name: "Rubocop"
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v6
13 | - uses: ruby/setup-ruby@v1
14 | with:
15 | ruby-version: "ruby"
16 | bundler-cache: true
17 | - run: bundle exec rubocop
18 | test:
19 | name: "Test / Ruby ${{ matrix.ruby }} / Rails ${{ matrix.rails }}"
20 | runs-on: ubuntu-latest
21 | strategy:
22 | matrix:
23 | rails: ["7.2", "8.0", "main"]
24 | ruby: ["3.2", "3.3", "3.4", "head"]
25 | exclude:
26 | - rails: "8.0"
27 | ruby: "3.1"
28 | - rails: main
29 | ruby: "3.1"
30 | env:
31 | BUNDLE_GEMFILE: gemfiles/rails_${{ matrix.rails }}.gemfile
32 | steps:
33 | - uses: actions/checkout@v6
34 | - uses: ruby/setup-ruby@v1
35 | with:
36 | ruby-version: ${{ matrix.ruby }}
37 | bundler-cache: true
38 | - run: bundle exec rake test
39 | test-all:
40 | name: "Test / All"
41 | runs-on: ubuntu-latest
42 | needs: [test]
43 | if: always()
44 | steps:
45 | - name: All tests ok
46 | if: ${{ !(contains(needs.*.result, 'failure')) }}
47 | run: exit 0
48 | - name: Some tests failed
49 | if: ${{ contains(needs.*.result, 'failure') }}
50 | run: exit 1
51 |
--------------------------------------------------------------------------------
/lib/configurable_from_env/environment_value.rb:
--------------------------------------------------------------------------------
1 | module ConfigurableFromEnv
2 | class EnvironmentValue
3 | TYPES = %i[boolean integer string].freeze
4 |
5 | BOOLEAN_VALUES = {}.merge(
6 | %w[1 true yes t y enable enabled on].to_h { [_1, true] },
7 | %w[0 false no f n disable disabled off].to_h { [_1, false] },
8 | { "" => false }
9 | ).freeze
10 |
11 | def self.from(definition)
12 | return nil if definition.nil?
13 |
14 | definition = { key: definition } unless definition.is_a?(Hash)
15 | definition.assert_valid_keys(:key, :type)
16 | new(**definition)
17 | end
18 |
19 | attr_reader :key, :type
20 |
21 | def initialize(key:, type: :string, env: ENV)
22 | unless TYPES.include?(type)
23 | raise ArgumentError, "Invalid type: #{type.inspect} (must be one of #{TYPES.map(&:inspect).join(', ')})"
24 | end
25 |
26 | @key = key
27 | @type = type
28 | @env = env
29 | end
30 |
31 | def read(required: true)
32 | if env.key?(key)
33 | value = convert(env[key])
34 | block_given? ? yield(value) : value
35 | elsif required
36 | raise ArgumentError, "Missing required environment variable: #{key}"
37 | end
38 | end
39 |
40 | private
41 |
42 | attr_reader :env
43 |
44 | def convert(value)
45 | send(:"convert_to_#{type}", value)
46 | rescue ArgumentError
47 | raise ArgumentError, "Environment variable #{key} has an invalid #{type} value: #{value.inspect}"
48 | end
49 |
50 | def convert_to_boolean(value)
51 | BOOLEAN_VALUES.fetch(value&.downcase&.strip) do
52 | raise ArgumentError, "Boolean value must be one of #{BOOLEAN_VALUES.keys.join(', ')}"
53 | end
54 | end
55 |
56 | def convert_to_integer(value)
57 | Integer(value)
58 | end
59 |
60 | def convert_to_string(value)
61 | value.to_s
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/test/configurable_from_env_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ConfigurableFromEnvTest < Minitest::Test
4 | def setup
5 | @class = Class.new
6 | @class.include ConfigurableFromEnv
7 | end
8 |
9 | def teardown
10 | ENV.delete("TEST_API_KEY")
11 | ENV.delete("TEST_TIMEOUT")
12 | end
13 |
14 | def test_defines_accessor_with_no_env_source
15 | @class.config_accessor :logger
16 |
17 | assert_respond_to @class, :logger
18 | assert_respond_to @class, :logger=
19 | end
20 |
21 | def test_defines_multiple_accessors_with_no_env_source
22 | @class.config_accessor :http_client, :logger
23 |
24 | assert_respond_to @class, :http_client
25 | assert_respond_to @class, :http_client=
26 | assert_respond_to @class, :logger
27 | assert_respond_to @class, :logger=
28 | end
29 |
30 | def test_raises_if_multiple_accessors_declared_with_one_env_option
31 | error = assert_raises(ArgumentError) { @class.config_accessor :api_key, :logger, from_env: "TEST_API_KEY" }
32 | assert_match "Only one accessor at a time can be created using the :from_env option", error.message
33 | end
34 |
35 | def test_defines_accessor_with_key_and_implicit_type
36 | @class.config_accessor :api_key, from_env: "TEST_API_KEY", default: nil
37 |
38 | assert_respond_to @class, :api_key
39 | assert_respond_to @class, :api_key=
40 | end
41 |
42 | def test_defines_accessor_with_key_and_type
43 | @class.config_accessor :timeout, from_env: { key: "TEST_TIMEOUT", type: :integer }, default: 30
44 |
45 | assert_respond_to @class, :timeout
46 | assert_respond_to @class, :timeout=
47 | end
48 |
49 | def test_uses_default_value_when_env_var_is_absent
50 | @class.config_accessor :timeout, from_env: { key: "TEST_TIMEOUT", type: :integer }, default: 30
51 |
52 | assert_equal 30, @class.timeout
53 | end
54 |
55 | def test_assigns_integer_env_var_value_overriding_default
56 | ENV["TEST_TIMEOUT"] = "5"
57 | @class.config_accessor :timeout, from_env: { key: "TEST_TIMEOUT", type: :integer }, default: 30
58 |
59 | assert_equal 5, @class.timeout
60 | end
61 |
62 | def test_assigns_string_env_var_value_with_no_default
63 | ENV["TEST_API_KEY"] = "secret"
64 | @class.config_accessor :api_key, from_env: "TEST_API_KEY"
65 |
66 | assert_equal "secret", @class.api_key
67 | end
68 |
69 | def test_raises_if_no_default_and_env_var_is_absent
70 | error = assert_raises(ArgumentError) { @class.config_accessor :api_key, from_env: "TEST_API_KEY" }
71 | assert_match "Missing required environment variable: TEST_API_KEY", error.message
72 | end
73 | end
74 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | plugins:
2 | - rubocop-minitest
3 | - rubocop-packaging
4 | - rubocop-performance
5 | - rubocop-rake
6 |
7 | AllCops:
8 | DisplayCopNames: true
9 | DisplayStyleGuide: true
10 | NewCops: enable
11 | TargetRubyVersion: 3.2
12 | Exclude:
13 | - "lib/configurable_from_env/configurable.rb"
14 | - "tmp/**/*"
15 | - "vendor/**/*"
16 |
17 | Bundler/OrderedGems:
18 | Exclude:
19 | - "gemfiles/*"
20 |
21 | Layout/FirstArrayElementIndentation:
22 | EnforcedStyle: consistent
23 |
24 | Layout/FirstArrayElementLineBreak:
25 | Enabled: true
26 |
27 | Layout/FirstHashElementLineBreak:
28 | Enabled: true
29 |
30 | Layout/FirstMethodArgumentLineBreak:
31 | Enabled: true
32 |
33 | Layout/HashAlignment:
34 | EnforcedColonStyle:
35 | - table
36 | - key
37 | EnforcedHashRocketStyle:
38 | - table
39 | - key
40 |
41 | Layout/MultilineArrayLineBreaks:
42 | Enabled: true
43 |
44 | Layout/MultilineHashKeyLineBreaks:
45 | Enabled: true
46 |
47 | Layout/MultilineMethodArgumentLineBreaks:
48 | Enabled: true
49 |
50 | Layout/MultilineMethodCallIndentation:
51 | EnforcedStyle: indented
52 |
53 | Layout/SpaceAroundEqualsInParameterDefault:
54 | EnforcedStyle: no_space
55 |
56 | Metrics/AbcSize:
57 | Max: 20
58 | Exclude:
59 | - "test/**/*"
60 |
61 | Metrics/BlockLength:
62 | Exclude:
63 | - "*.gemspec"
64 | - "Rakefile"
65 |
66 | Metrics/ClassLength:
67 | Exclude:
68 | - "test/**/*"
69 |
70 | Metrics/MethodLength:
71 | Max: 18
72 | Exclude:
73 | - "test/**/*"
74 |
75 | Metrics/ParameterLists:
76 | Max: 6
77 |
78 | Minitest/AssertTruthy:
79 | Enabled: false
80 |
81 | Minitest/EmptyLineBeforeAssertionMethods:
82 | Enabled: false
83 |
84 | Minitest/MultipleAssertions:
85 | Max: 5
86 |
87 | Minitest/RefuteFalse:
88 | Enabled: false
89 |
90 | Naming/MemoizedInstanceVariableName:
91 | Enabled: false
92 |
93 | Naming/VariableNumber:
94 | Enabled: false
95 |
96 | Rake/Desc:
97 | Enabled: false
98 |
99 | Style/BarePercentLiterals:
100 | EnforcedStyle: percent_q
101 |
102 | Style/ClassAndModuleChildren:
103 | Enabled: false
104 |
105 | Style/Documentation:
106 | Enabled: false
107 |
108 | Style/DoubleNegation:
109 | Enabled: false
110 |
111 | Style/EmptyMethod:
112 | Enabled: false
113 |
114 | Style/FrozenStringLiteralComment:
115 | Enabled: false
116 |
117 | Style/NumericPredicate:
118 | Enabled: false
119 |
120 | Style/StringLiterals:
121 | EnforcedStyle: double_quotes
122 |
123 | Style/TrivialAccessors:
124 | AllowPredicates: true
125 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rake/testtask"
3 | require "rubocop/rake_task"
4 |
5 | Rake::TestTask.new(:test) do |t|
6 | t.libs << "test"
7 | t.libs << "lib"
8 | t.test_files = FileList["test/**/*_test.rb"]
9 | end
10 |
11 | RuboCop::RakeTask.new
12 |
13 | task default: %i[test rubocop]
14 |
15 | # == "rake release" enhancements ==============================================
16 |
17 | Rake::Task["release"].enhance do
18 | puts "Don't forget to publish the release on GitHub!"
19 | system "open https://github.com/mattbrictson/configurable_from_env/releases"
20 | end
21 |
22 | task :disable_overcommit do
23 | ENV["OVERCOMMIT_DISABLE"] = "1"
24 | end
25 |
26 | Rake::Task[:build].enhance [:disable_overcommit]
27 |
28 | task :verify_gemspec_files do
29 | git_files = `git ls-files -z`.split("\x0")
30 | gemspec_files = Gem::Specification.load("configurable_from_env.gemspec").files.sort
31 | ignored_by_git = gemspec_files - git_files
32 | next if ignored_by_git.empty?
33 |
34 | raise <<~ERROR
35 |
36 | The `spec.files` specified in configurable_from_env.gemspec include the following files
37 | that are being ignored by git. Did you forget to add them to the repo? If
38 | not, you may need to delete these files or modify the gemspec to ensure
39 | that they are not included in the gem by mistake:
40 |
41 | #{ignored_by_git.join("\n").gsub(/^/, ' ')}
42 |
43 | ERROR
44 | end
45 |
46 | Rake::Task[:build].enhance [:verify_gemspec_files]
47 |
48 | # == "rake bump" tasks ========================================================
49 |
50 | task bump: %w[bump:bundler bump:ruby bump:year]
51 |
52 | namespace :bump do
53 | task :bundler do
54 | sh "bundle update --bundler"
55 | end
56 |
57 | task :ruby do
58 | replace_in_file "configurable_from_env.gemspec", /ruby_version = .*">= (.*)"/ => RubyVersions.lowest
59 | replace_in_file ".rubocop.yml", /TargetRubyVersion: (.*)/ => RubyVersions.lowest
60 | replace_in_file ".github/workflows/ci.yml", /ruby: (\[.+\])/ => RubyVersions.all.inspect
61 | end
62 |
63 | task :year do
64 | replace_in_file "LICENSE.txt", /\(c\) (\d+)/ => Date.today.year.to_s
65 | end
66 | end
67 |
68 | require "date"
69 | require "open-uri"
70 | require "yaml"
71 |
72 | def replace_in_file(path, replacements)
73 | contents = File.read(path)
74 | orig_contents = contents.dup
75 | replacements.each do |regexp, text|
76 | raise "Can't find #{regexp} in #{path}" unless regexp.match?(contents)
77 |
78 | contents.gsub!(regexp) do |match|
79 | match[regexp, 1] = text
80 | match
81 | end
82 | end
83 | File.write(path, contents) if contents != orig_contents
84 | end
85 |
86 | module RubyVersions
87 | class << self
88 | def lowest
89 | all.first
90 | end
91 |
92 | def all
93 | patches = versions.values_at(:stable, :security_maintenance).compact.flatten
94 | sorted_minor_versions = patches.map { |p| p[/\d+\.\d+/] }.sort_by(&:to_f)
95 | [*sorted_minor_versions, "head"]
96 | end
97 |
98 | private
99 |
100 | def versions
101 | @_versions ||= begin
102 | yaml = URI.open("https://raw.githubusercontent.com/ruby/www.ruby-lang.org/HEAD/_data/downloads.yml")
103 | YAML.safe_load(yaml, symbolize_names: true)
104 | end
105 | end
106 | end
107 | end
108 |
--------------------------------------------------------------------------------
/test/configurable_from_env/environment_value_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | module ConfigurableFromEnv
4 | class EnvironmentValueTest < Minitest::Test
5 | def test_from_implicit_type
6 | value = EnvironmentValue.from("TEST")
7 |
8 | assert_equal "TEST", value.key
9 | assert_equal :string, value.type
10 | end
11 |
12 | def test_from_explicit_type
13 | value = EnvironmentValue.from({ key: "TEST", type: :boolean })
14 |
15 | assert_equal "TEST", value.key
16 | assert_equal :boolean, value.type
17 | end
18 |
19 | def test_from_invalid_options
20 | error = assert_raises(ArgumentError) { EnvironmentValue.from({ key: "TEST", type: :boolean, optional: true }) }
21 | assert_match "Unknown key: :optional. Valid keys are: :key, :type", error.message
22 | end
23 |
24 | def test_from_unknown_type
25 | error = assert_raises(ArgumentError) { EnvironmentValue.from({ key: "TEST", type: :date }) }
26 | assert_match "Invalid type: :date (must be one of :boolean, :integer, :string)", error.message
27 | end
28 |
29 | def test_read_string
30 | value = EnvironmentValue.new(key: "TEST", type: :string, env: { "TEST" => "hello" })
31 |
32 | assert_equal "hello", value.read
33 | end
34 |
35 | def test_read_yields_value
36 | value = EnvironmentValue.new(key: "TEST", type: :string, env: { "TEST" => "hello" })
37 | yielded = :nothing
38 | value.read { yielded = _1 }
39 |
40 | assert_equal "hello", yielded
41 | end
42 |
43 | def test_read_required_value_raises_if_env_var_is_absent
44 | value = EnvironmentValue.new(key: "TEST", type: :string, env: {})
45 |
46 | error = assert_raises(ArgumentError) { value.read(required: true) }
47 | assert_match "Missing required environment variable: TEST", error.message
48 | end
49 |
50 | def test_read_optional_value_returns_nil_and_does_not_yield_if_env_var_is_absent
51 | value = EnvironmentValue.new(key: "TEST", type: :string, env: {})
52 |
53 | yielded = :nothing
54 | returned = value.read(required: false) { yielded = _1 }
55 |
56 | assert_nil returned
57 | assert_equal :nothing, yielded
58 | end
59 |
60 | def test_read_true_booleans
61 | %w[1 true yes t y enable enabled on].each do |env_value|
62 | value = EnvironmentValue.new(key: "TEST", type: :boolean, env: { "TEST" => env_value })
63 |
64 | assert_equal true, value.read
65 | end
66 | end
67 |
68 | def test_read_false_booleans
69 | %w[0 false no f n disable disabled off].each do |env_value|
70 | value = EnvironmentValue.new(key: "TEST", type: :boolean, env: { "TEST" => env_value })
71 |
72 | assert_equal false, value.read
73 | end
74 | end
75 |
76 | def test_read_empty_string_is_false_boolean
77 | value = EnvironmentValue.new(key: "TEST", type: :boolean, env: { "TEST" => "" })
78 |
79 | assert_equal false, value.read
80 | end
81 |
82 | def test_read_upper_case_boolean
83 | value = EnvironmentValue.new(key: "TEST", type: :boolean, env: { "TEST" => "YES" })
84 |
85 | assert_equal true, value.read
86 | end
87 |
88 | def test_read_invalid_boolean
89 | value = EnvironmentValue.new(key: "TEST", type: :boolean, env: { "TEST" => "what" })
90 |
91 | error = assert_raises(ArgumentError) { value.read }
92 | assert_match 'Environment variable TEST has an invalid boolean value: "what"', error.message
93 | end
94 |
95 | def test_read_valid_integer
96 | value = EnvironmentValue.new(key: "TEST", type: :integer, env: { "TEST" => "25" })
97 |
98 | assert_equal 25, value.read
99 | end
100 |
101 | def test_read_valid_negative_integer
102 | value = EnvironmentValue.new(key: "TEST", type: :integer, env: { "TEST" => "-1" })
103 |
104 | assert_equal(-1, value.read)
105 | end
106 |
107 | def test_read_invalid_integer
108 | value = EnvironmentValue.new(key: "TEST", type: :integer, env: { "TEST" => "1A" })
109 |
110 | error = assert_raises(ArgumentError) { value.read }
111 | assert_match 'Environment variable TEST has an invalid integer value: "1A"', error.message
112 | end
113 | end
114 | end
115 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8 |
9 | ## Our Standards
10 |
11 | Examples of behavior that contributes to a positive environment for our community include:
12 |
13 | * Demonstrating empathy and kindness toward other people
14 | * Being respectful of differing opinions, viewpoints, and experiences
15 | * Giving and gracefully accepting constructive feedback
16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17 | * Focusing on what is best not just for us as individuals, but for the overall community
18 |
19 | Examples of unacceptable behavior include:
20 |
21 | * The use of sexualized language or imagery, and sexual attention or
22 | advances of any kind
23 | * Trolling, insulting or derogatory comments, and personal or political attacks
24 | * Public or private harassment
25 | * Publishing others' private information, such as a physical or email
26 | address, without their explicit permission
27 | * Other conduct which could reasonably be considered inappropriate in a
28 | professional setting
29 |
30 | ## Enforcement Responsibilities
31 |
32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33 |
34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35 |
36 | ## Scope
37 |
38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39 |
40 | ## Enforcement
41 |
42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at opensource@mattbrictson.com. All complaints will be reviewed and investigated promptly and fairly.
43 |
44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45 |
46 | ## Enforcement Guidelines
47 |
48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49 |
50 | ### 1. Correction
51 |
52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53 |
54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55 |
56 | ### 2. Warning
57 |
58 | **Community Impact**: A violation through a single incident or series of actions.
59 |
60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61 |
62 | ### 3. Temporary Ban
63 |
64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65 |
66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67 |
68 | ### 4. Permanent Ban
69 |
70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71 |
72 | **Consequence**: A permanent ban from any sort of public interaction within the community.
73 |
74 | ## Attribution
75 |
76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78 |
79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80 |
81 | [homepage]: https://www.contributor-covenant.org
82 |
83 | For answers to common questions about this code of conduct, see the FAQ at
84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
85 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # configurable_from_env
2 |
3 | [](https://rubygems.org/gems/configurable_from_env)
4 | [](https://www.ruby-toolbox.com/projects/configurable_from_env)
5 | [](https://github.com/mattbrictson/configurable_from_env/actions/workflows/ci.yml)
6 |
7 | The `configurable_from_env` gem allows you to define accessors that automatically populate via environment variables. It brings back Active Support's [`config_accessor`](https://github.com/rails/rails/blob/819a94934966eafb6bee6990b18372e1eb91159d/activesupport/lib/active_support/configurable.rb#L111) – which was [deprecated](https://github.com/rails/rails/pull/53970) in Rails 8.1 – and enhances it with a new `:from_env` option.
8 |
9 | > [!NOTE]
10 | > This project is experimental. Please open an issue or send me an email and let me know what you think!
11 |
12 | ---
13 |
14 | - [Quick start](#quick-start)
15 | - [Motivation](#motivation)
16 | - [Support](#support)
17 | - [License](#license)
18 | - [Code of conduct](#code-of-conduct)
19 | - [Contribution guide](#contribution-guide)
20 |
21 | ## Quick start
22 |
23 | Add the gem to your Gemfile and run `bundle install`:
24 |
25 | ```ruby
26 | gem "configurable_from_env"
27 | ```
28 |
29 | Include the `ConfigurableFromEnv` mixin and then use `config_accessor` to define configurable attributes that are automatically populated from the environment.
30 |
31 | ```ruby
32 | class MyHttpClient
33 | include ConfigurableFromEnv
34 |
35 | # Define an api_key accessor that is automatically populated from ENV["MY_API_KEY"]
36 | config_accessor :api_key, from_env: "MY_API_KEY"
37 |
38 | # Validate and convert ENV value into a desired data type
39 | config_accessor :timeout, from_env: { key: "MY_TIMEOUT", type: :integer }
40 |
41 | # Fall back to a default value if the environment variable is absent
42 | config_accessor :verify_tls, from_env: { key: "MY_VERIFY_TLS", type: :boolean }, default: true
43 |
44 | def fetch_data
45 | # Config attributes are exposed as instance methods for easy access
46 | conn = Faraday.new(
47 | headers: { "X-API-Key" => api_key },
48 | request: { timeout: timeout },
49 | ssl: { verify: verify_tls }
50 | )
51 | conn.get("https://my.api/data")
52 | end
53 | end
54 | ```
55 |
56 | ## Motivation
57 |
58 | Rails lacks a simple way to declare configuration dependencies on `ENV` values. Generally, the framework guides you toward one of two common approaches:
59 |
60 | **Use `ENV.fetch` directly when you need a value.** This is the most direct technique, but it means that `ENV` dependencies are scattered throughout implementation code. Testing can become more difficult due to this implicit global variable dependency, and you may need a special library to mock `ENV` access.
61 |
62 | **Define a configuration object, and use an initializer to copy values from `ENV` into the config.** Third-party gems often use this approach. Consider Devise, which uses `config/initializers/devise.rb`.
63 |
64 | ```ruby
65 | Devise.setup do |config|
66 | config.secret_key = ENV.fetch("DEVISE_SECRET_KEY")
67 | ```
68 |
69 | This is very flexible, and works well for libraries that need to be portable across many different app environments. However for application-level code it can repetitive, as each configuration attribute has to be declared multiple times, often in 3 separate files:
70 |
71 | 1. Define a configuration class that declares the attribute.
72 | 2. Create an initializer that sets the attribute.
73 | 3. Reference the configuration when using the attribute.
74 |
75 | **Regardless of where you put the `ENV` access, validating and converting values can be tedious.** Environment values are always strings, but often we need them to configure settings that are booleans or integers.
76 |
77 | ### A different approach
78 |
79 | `configurable_from_env` is an extremely lightweight solution (~80 LOC) to the shortcomings listed above. Consider this example:
80 |
81 | ```ruby
82 | class MyHttpClient
83 | include ConfigurableFromEnv
84 | config_accessor :timeout, from_env: { key: "MY_TIMEOUT", type: :integer }, default: 30
85 | ```
86 |
87 | The benefits are:
88 |
89 | - Configurable attributes, their default values, how they map from environment variables, and their data types are all declared in one place, as opposed to scattered across initializers, classes, and/or YAML files.
90 | - The configuration is colocated with the code where it used (i.e. the `MyHttpClient` class, in the example).
91 | - Because these are simple accessors, values can be easily injected in unit tests without needing to mock `ENV`.
92 | - Specifying a `:type` takes care of validation and conversion of environment values with an intuitive and concise syntax.
93 |
94 | ## Support
95 |
96 | If you want to report a bug, or have ideas, feedback or questions about the gem, [let me know via GitHub issues](https://github.com/mattbrictson/configurable_from_env/issues/new) and I will do my best to provide a helpful answer. Happy hacking!
97 |
98 | ## License
99 |
100 | The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
101 |
102 | ## Code of conduct
103 |
104 | Everyone interacting in this project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
105 |
106 | ## Contribution guide
107 |
108 | Pull requests are welcome!
109 |
--------------------------------------------------------------------------------
/lib/configurable_from_env/configurable.rb:
--------------------------------------------------------------------------------
1 | # Copied from https://github.com/rails/rails/blob/8-0-stable/activesupport/lib/active_support/configurable.rb
2 | #
3 | # Copyright (c) David Heinemeier Hansson
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.
23 |
24 | require "active_support"
25 | require "active_support/concern"
26 | require "active_support/ordered_options"
27 |
28 | module ConfigurableFromEnv
29 | # = Active Support \Configurable
30 | #
31 | # Configurable provides a config method to store and retrieve
32 | # configuration options as an OrderedOptions.
33 | module Configurable
34 | extend ActiveSupport::Concern
35 |
36 | class Configuration < ActiveSupport::InheritableOptions
37 | def compile_methods!
38 | self.class.compile_methods!(keys)
39 | end
40 |
41 | # Compiles reader methods so we don't have to go through method_missing.
42 | def self.compile_methods!(keys)
43 | keys.reject { |m| method_defined?(m) }.each do |key|
44 | class_eval <<-RUBY, __FILE__, __LINE__ + 1
45 | def #{key}; _get(#{key.inspect}); end
46 | RUBY
47 | end
48 | end
49 | end
50 |
51 | module ClassMethods
52 | def config
53 | @_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
54 | superclass.config.inheritable_copy
55 | else
56 | # create a new "anonymous" class that will host the compiled reader methods
57 | Class.new(Configuration).new
58 | end
59 | end
60 |
61 | def configure
62 | yield config
63 | end
64 |
65 | # Allows you to add shortcut so that you don't have to refer to attribute
66 | # through config. Also look at the example for config to contrast.
67 | #
68 | # Defines both class and instance config accessors.
69 | #
70 | # class User
71 | # include ActiveSupport::Configurable
72 | # config_accessor :allowed_access
73 | # end
74 | #
75 | # User.allowed_access # => nil
76 | # User.allowed_access = false
77 | # User.allowed_access # => false
78 | #
79 | # user = User.new
80 | # user.allowed_access # => false
81 | # user.allowed_access = true
82 | # user.allowed_access # => true
83 | #
84 | # User.allowed_access # => false
85 | #
86 | # The attribute name must be a valid method name in Ruby.
87 | #
88 | # class User
89 | # include ActiveSupport::Configurable
90 | # config_accessor :"1_Badname"
91 | # end
92 | # # => NameError: invalid config attribute name
93 | #
94 | # To omit the instance writer method, pass instance_writer: false.
95 | # To omit the instance reader method, pass instance_reader: false.
96 | #
97 | # class User
98 | # include ActiveSupport::Configurable
99 | # config_accessor :allowed_access, instance_reader: false, instance_writer: false
100 | # end
101 | #
102 | # User.allowed_access = false
103 | # User.allowed_access # => false
104 | #
105 | # User.new.allowed_access = true # => NoMethodError
106 | # User.new.allowed_access # => NoMethodError
107 | #
108 | # Or pass instance_accessor: false, to omit both instance methods.
109 | #
110 | # class User
111 | # include ActiveSupport::Configurable
112 | # config_accessor :allowed_access, instance_accessor: false
113 | # end
114 | #
115 | # User.allowed_access = false
116 | # User.allowed_access # => false
117 | #
118 | # User.new.allowed_access = true # => NoMethodError
119 | # User.new.allowed_access # => NoMethodError
120 | #
121 | # Also you can pass default or a block to set up the attribute with a default value.
122 | #
123 | # class User
124 | # include ActiveSupport::Configurable
125 | # config_accessor :allowed_access, default: false
126 | # config_accessor :hair_colors do
127 | # [:brown, :black, :blonde, :red]
128 | # end
129 | # end
130 | #
131 | # User.allowed_access # => false
132 | # User.hair_colors # => [:brown, :black, :blonde, :red]
133 | def config_accessor(*names, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil) # :doc:
134 | names.each do |name|
135 | raise NameError.new("invalid config attribute name") unless /\A[_A-Za-z]\w*\z/.match?(name)
136 |
137 | reader, reader_line = "def #{name}; config.#{name}; end", __LINE__
138 | writer, writer_line = "def #{name}=(value); config.#{name} = value; end", __LINE__
139 |
140 | singleton_class.class_eval reader, __FILE__, reader_line
141 | singleton_class.class_eval writer, __FILE__, writer_line
142 |
143 | if instance_accessor
144 | class_eval reader, __FILE__, reader_line if instance_reader
145 | class_eval writer, __FILE__, writer_line if instance_writer
146 | end
147 |
148 | send("#{name}=", block_given? ? yield : default)
149 | end
150 | end
151 | private :config_accessor
152 |
153 | private
154 | def inherited(subclass)
155 | super
156 | subclass.class_eval do
157 | @_config = nil
158 | end
159 | end
160 | end
161 |
162 | # Reads and writes attributes from a configuration OrderedOptions.
163 | #
164 | # require "active_support/configurable"
165 | #
166 | # class User
167 | # include ActiveSupport::Configurable
168 | # end
169 | #
170 | # user = User.new
171 | #
172 | # user.config.allowed_access = true
173 | # user.config.level = 1
174 | #
175 | # user.config.allowed_access # => true
176 | # user.config.level # => 1
177 | def config
178 | @_config ||= self.class.config.inheritable_copy
179 | end
180 | end
181 | end
182 |
--------------------------------------------------------------------------------