configurable_from_env 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c87398f6f8863cf380719eb8e54029579cbc2941476a97b522b45a256ab996da
4
+ data.tar.gz: 8cbf8cf83eb5ce937b22b874fecd12e0ea8ed8161944df184cf79b0ec4d6e595
5
+ SHA512:
6
+ metadata.gz: 2dfeb692f554bd8db7c5ac4d2c3c605f0eec120a82ea9565468b4a1b5c0b508c1c5bc498488719cdd5980f08e26d200a11f71c0abf6e6fe41577b6f21f09156e
7
+ data.tar.gz: ee98578923a3262c77e75eece7a8f7bee05f268738a2b4f81d488507f9ed2ca9c188c10856ec2c20b04a1a660b1df68b36313f63191110eb665aabef08d9a384
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 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.
data/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # configurable_from_env
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/configurable_from_env)](https://rubygems.org/gems/configurable_from_env)
4
+ [![Gem Downloads](https://img.shields.io/gem/dt/configurable_from_env)](https://www.ruby-toolbox.com/projects/configurable_from_env)
5
+ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/mattbrictson/configurable_from_env/ci.yml)](https://github.com/mattbrictson/configurable_from_env/actions/workflows/ci.yml)
6
+ [![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability/mattbrictson/configurable_from_env)](https://codeclimate.com/github/mattbrictson/configurable_from_env)
7
+
8
+ The `configurable_from_env` gem allows you to define accessors that automatically populate via environment variables. It works by extending Active Support's [`config_accessor`](https://api.rubyonrails.org/classes/ActiveSupport/Configurable/ClassMethods.html#method-i-config_accessor) with a new `:from_env` option.
9
+
10
+ > [!NOTE]
11
+ > This project is experimental. Please open an issue or send me an email and let me know what you think!
12
+
13
+ ---
14
+
15
+ - [Quick start](#quick-start)
16
+ - [Motivation](#motivation)
17
+ - [Support](#support)
18
+ - [License](#license)
19
+ - [Code of conduct](#code-of-conduct)
20
+ - [Contribution guide](#contribution-guide)
21
+
22
+ ## Quick start
23
+
24
+ Add the gem to your Gemfile and run `bundle install`:
25
+
26
+ ```ruby
27
+ gem "configurable_from_env"
28
+ ```
29
+
30
+ Include the `ConfigurableFromEnv` mixin and then use `config_accessor` to define configurable attributes that are automatically populated from the environment.
31
+
32
+ ```ruby
33
+ class MyHttpClient
34
+ include ConfigurableFromEnv
35
+
36
+ # Define an api_key accessor that is automatically populated from ENV["MY_API_KEY"]
37
+ config_accessor :api_key, from_env: "MY_API_KEY"
38
+
39
+ # Validate and convert ENV value into a desired data type
40
+ config_accessor :timeout, from_env: { key: "MY_TIMEOUT", type: :integer }
41
+
42
+ # Fall back to a default value if the environment variable is absent
43
+ config_accessor :verify_tls, from_env: { key: "MY_VERIFY_TLS", type: :boolean }, default: true
44
+
45
+ def fetch_data
46
+ # Config attributes are exposed as instance methods for easy access
47
+ conn = Faraday.new(
48
+ headers: { "X-API-Key" => api_key },
49
+ request: { timeout: timeout },
50
+ ssl: { verify: verify_tls }
51
+ )
52
+ conn.get("https://my.api/data")
53
+ end
54
+ end
55
+ ```
56
+
57
+ ## Motivation
58
+
59
+ Rails lacks a simple way to declare configuration dependencies on `ENV` values. Generally, the framework guides you toward one of two common approaches:
60
+
61
+ **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.
62
+
63
+ **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`.
64
+
65
+ ```ruby
66
+ Devise.setup do |config|
67
+ config.secret_key = ENV.fetch("DEVISE_SECRET_KEY")
68
+ ```
69
+
70
+ 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:
71
+
72
+ 1. Define a configuration class that declares the attribute.
73
+ 2. Create an initializer that sets the attribute.
74
+ 3. Reference the configuration when using the attribute.
75
+
76
+ **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.
77
+
78
+ ### A different approach
79
+
80
+ `configurable_from_env` is an extremely lightweight solution (~80 LOC) to the shortcomings listed above. Consider this example:
81
+
82
+ ```ruby
83
+ class MyHttpClient
84
+ include ConfigurableFromEnv
85
+ config_accessor :timeout, from_env: { key: "MY_TIMEOUT", type: :integer }, default: 30
86
+ ```
87
+
88
+ The benefits are:
89
+
90
+ - 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.
91
+ - The configuration is colocated with the code where it used (i.e. the `MyHttpClient` class, in the example).
92
+ - Because these are simple accessors, values can be easily injected in unit tests without needing to mock `ENV`.
93
+ - Specifying a `:type` takes care of validation and conversion of environment values with an intuitive and concise syntax.
94
+
95
+ ## Support
96
+
97
+ 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!
98
+
99
+ ## License
100
+
101
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
102
+
103
+ ## Code of conduct
104
+
105
+ 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).
106
+
107
+ ## Contribution guide
108
+
109
+ Pull requests are welcome!
@@ -0,0 +1,64 @@
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
@@ -0,0 +1,3 @@
1
+ module ConfigurableFromEnv
2
+ VERSION = "0.1.0".freeze
3
+ end
@@ -0,0 +1,31 @@
1
+ require "active_support"
2
+ require "active_support/concern"
3
+ require "active_support/configurable"
4
+ require "active_support/core_ext/enumerable"
5
+ require "active_support/core_ext/hash/keys"
6
+
7
+ module ConfigurableFromEnv
8
+ autoload :EnvironmentValue, "configurable_from_env/environment_value"
9
+ autoload :VERSION, "configurable_from_env/version"
10
+
11
+ extend ActiveSupport::Concern
12
+ include ActiveSupport::Configurable
13
+
14
+ module ClassMethods
15
+ def config_accessor(*attributes, from_env: nil, **options, &block)
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, &block)
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
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: configurable_from_env
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt Brictson
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2024-11-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.2'
27
+ description:
28
+ email:
29
+ - opensource@mattbrictson.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE.txt
35
+ - README.md
36
+ - lib/configurable_from_env.rb
37
+ - lib/configurable_from_env/environment_value.rb
38
+ - lib/configurable_from_env/version.rb
39
+ homepage: https://github.com/mattbrictson/configurable_from_env
40
+ licenses:
41
+ - MIT
42
+ metadata:
43
+ bug_tracker_uri: https://github.com/mattbrictson/configurable_from_env/issues
44
+ changelog_uri: https://github.com/mattbrictson/configurable_from_env/releases
45
+ source_code_uri: https://github.com/mattbrictson/configurable_from_env
46
+ homepage_uri: https://github.com/mattbrictson/configurable_from_env
47
+ rubygems_mfa_required: 'true'
48
+ post_install_message:
49
+ rdoc_options: []
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '3.1'
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ requirements: []
63
+ rubygems_version: 3.5.23
64
+ signing_key:
65
+ specification_version: 4
66
+ summary: Define accessors that are automatically populated via ENV
67
+ test_files: []