configsl 1.0.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 +7 -0
- data/CHANGELOG.md +20 -0
- data/Gemfile +25 -0
- data/README.md +105 -0
- data/lib/configsl/config.rb +26 -0
- data/lib/configsl/dsl.rb +95 -0
- data/lib/configsl/exception.rb +5 -0
- data/lib/configsl/file_format/base.rb +33 -0
- data/lib/configsl/file_format/json.rb +22 -0
- data/lib/configsl/file_format/yaml.rb +22 -0
- data/lib/configsl/file_support.rb +124 -0
- data/lib/configsl/format.rb +60 -0
- data/lib/configsl/from_environment.rb +51 -0
- data/lib/configsl/from_file.rb +56 -0
- data/lib/configsl/validation.rb +42 -0
- data/lib/configsl.rb +7 -0
- metadata +78 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9cf52e102562625f71dabce8ff0f05d8d1a0d129e62584a80aae1abd4903585d
|
4
|
+
data.tar.gz: f59fcc77eb986087131fe84e6770bc667be007ef922cb77477ab54b840e1a75b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 49f9b7c79beae87c9aff4be256de82fafa0f5fe6e76f6243f48c25ded6c0363df8330874ddcb6b7f7719ed2fd9d5ea02d4598491acb1d359c151656d25f060fe
|
7
|
+
data.tar.gz: afec6d65e82afe81485aa89bf73878e1d95deb96dc04db343edbb9cec980332738743208994a3d57ee2bedfd8f69f124187c134d3fb137a83ee7581920b2eb4b
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
All notable changes to this project will be documented in this file.
|
4
|
+
|
5
|
+
The format is based on [Keep a Changelog][changelog], and this project adheres
|
6
|
+
to [Semantic Versioning][versioning].
|
7
|
+
ß
|
8
|
+
## 1.0.0
|
9
|
+
|
10
|
+
Initial release.
|
11
|
+
|
12
|
+
### Added
|
13
|
+
|
14
|
+
- Simple DSL for defining configuration
|
15
|
+
- JSON and YAML file reading support
|
16
|
+
- Environment variable reading support
|
17
|
+
- Formatting and validation of values
|
18
|
+
|
19
|
+
[changelog]: https://keepachangelog.com/en/1.1.0/
|
20
|
+
[versioning]: https://semver.org/spec/v2.0.0.html
|
data/Gemfile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
gemspec
|
6
|
+
|
7
|
+
group :development do
|
8
|
+
gem 'rake', '~> 13.2'
|
9
|
+
gem 'rubocop', '~> 1.65'
|
10
|
+
gem 'rubocop-factory_bot', '~> 2.26'
|
11
|
+
gem 'rubocop-rake', '~> 0.6'
|
12
|
+
gem 'rubocop-rspec', '~> 3.0'
|
13
|
+
end
|
14
|
+
|
15
|
+
group :test do
|
16
|
+
# activesupport 7.2 introduces a breaking change that causes the specs to
|
17
|
+
# fail.
|
18
|
+
gem 'activesupport', '~> 7.1.0'
|
19
|
+
|
20
|
+
gem 'coveralls_reborn', '~> 0.28'
|
21
|
+
gem 'factory_bot', '~> 6.4'
|
22
|
+
gem 'rspec', '~> 3.13'
|
23
|
+
gem 'rspec-github', '~> 2.4'
|
24
|
+
gem 'simplecov', '~> 0.22'
|
25
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# ConfigSL [![Coverage Status][badge-coverage]][coverage]
|
2
|
+
|
3
|
+
ConfigSL is a simple Domain-Specific Language (DSL) module for configuration.
|
4
|
+
It is designed to provide a declarative way to define configuration, with as few
|
5
|
+
dependencies and additional cruft as possible. It is both modular and
|
6
|
+
extensible, so you can use as little or as much as you need.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add this line to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem 'configsl'
|
14
|
+
```
|
15
|
+
|
16
|
+
And then execute:
|
17
|
+
|
18
|
+
```sh
|
19
|
+
bundle install
|
20
|
+
```
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
```sh
|
25
|
+
gem install configsl
|
26
|
+
```
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
You can start defining your configurations using two methods:
|
31
|
+
|
32
|
+
1. Extend the included `ConfigSL::Config` base class
|
33
|
+
2. Include the `ConfigSL` modules you want to use in you class
|
34
|
+
|
35
|
+
### Using the included base class
|
36
|
+
|
37
|
+
The `ConfigSL::Config` base class includes common functionality for working with
|
38
|
+
configurations. Currently, the class provides the following features:
|
39
|
+
|
40
|
+
- **DSL**: The primary DSL for defining configuration options
|
41
|
+
- **Format**: A simple way to enforce option value formatting
|
42
|
+
- **FromEnvironment**: Load configuration from environment variables
|
43
|
+
- **FromFile**: Load configuration from a file
|
44
|
+
- **Validation**: Built-in validation for configuration options
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require 'configsl'
|
48
|
+
|
49
|
+
class AppConfig < ConfigSL::Config
|
50
|
+
register_file_format :json
|
51
|
+
register_file_format :yaml
|
52
|
+
|
53
|
+
option :name, type: String, default: 'My App'
|
54
|
+
option :environment, type: Symbol, enum: %i[dev test prod], default: :dev,
|
55
|
+
env_variable: 'RACK_ENV'
|
56
|
+
option :database, type: DatabaseConfig, required: true
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
### Including modules
|
61
|
+
|
62
|
+
If you'd like to pick and choose the features you want to use, you can include
|
63
|
+
modules individually. _Most_ modules can be included in any order, but the `DSL`
|
64
|
+
module _**must**_ be included before any others.
|
65
|
+
|
66
|
+
Additionally, you will need to implement `initialize` -- or some other method --
|
67
|
+
that sets the configuration values by calling `set_value` for each option.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
require 'configsl'
|
71
|
+
|
72
|
+
class ApplicationConfig
|
73
|
+
include ConfigSL::DSL
|
74
|
+
include ConfigSL::Format
|
75
|
+
include ConfigSL::FromEnvironment
|
76
|
+
|
77
|
+
option :name, type: String, default: 'My App'
|
78
|
+
option :environment, type: Symbol, env_variable: 'RACK_ENV'
|
79
|
+
option :database, type: DatabaseConfig
|
80
|
+
|
81
|
+
def initialize(params = {})
|
82
|
+
params.each do |name, value|
|
83
|
+
set_value(name, value)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
### A note about inheritance
|
90
|
+
|
91
|
+
When working with multiple configuration classes, you'll likely want to use a
|
92
|
+
base class. This could be the `ConfigSL::Config` class, or a custom class that
|
93
|
+
includes the modules you need.
|
94
|
+
|
95
|
+
While the modules you include are inherited by subclasses, any options you
|
96
|
+
define via DSL are not. This is because these values are stored using _class
|
97
|
+
instance variables_. As a result, if you have options that are shared between
|
98
|
+
classes, they will need to be implemented in both.
|
99
|
+
|
100
|
+
It's important to note that this is not limited to your defined configuration
|
101
|
+
options, but also methods such as `register_file_format` and
|
102
|
+
`config_file_path`.
|
103
|
+
|
104
|
+
[badge-coverage]: https://coveralls.io/repos/github/jamesiarmes/configsl/badge.svg
|
105
|
+
[coverage]: https://coveralls.io/github/jamesiarmes/configsl
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'dsl'
|
4
|
+
require_relative 'file_format/json'
|
5
|
+
require_relative 'file_format/yaml'
|
6
|
+
require_relative 'format'
|
7
|
+
require_relative 'from_environment'
|
8
|
+
require_relative 'from_file'
|
9
|
+
require_relative 'validation'
|
10
|
+
|
11
|
+
module ConfigSL
|
12
|
+
# Base class for configuration that includes common functionality.
|
13
|
+
class Config
|
14
|
+
include DSL
|
15
|
+
include Format
|
16
|
+
include FromEnvironment
|
17
|
+
include FromFile
|
18
|
+
include Validation
|
19
|
+
|
20
|
+
def initialize(params = {})
|
21
|
+
params.each do |name, value|
|
22
|
+
set_value(name, value)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/configsl/dsl.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'exception'
|
4
|
+
|
5
|
+
module ConfigSL
|
6
|
+
# DSL (Domain Specific Language) for defining configuration options.
|
7
|
+
#
|
8
|
+
# This module provides the base functionality for building configuration
|
9
|
+
# classes. It should be included before any other modules that provide
|
10
|
+
# additional functionality, such as formatting or validation.
|
11
|
+
#
|
12
|
+
# The class that includes this module will need to set the values for the
|
13
|
+
# options before they can be used. The easiest way to do this is to call
|
14
|
+
# `set_value` with each option and its value in the constructor.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# def initialize(params = {})
|
18
|
+
# params.each do |name, value|
|
19
|
+
# set_value(name, value)
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
module DSL
|
23
|
+
# Include the class methods when the module is included.
|
24
|
+
def self.included(base)
|
25
|
+
base.extend ClassMethods
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the options hash for the class of the current instance.
|
29
|
+
#
|
30
|
+
# @return [Hash] The options hash.
|
31
|
+
def options
|
32
|
+
self.class.options
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the values of all the options.
|
36
|
+
#
|
37
|
+
# @return [Array<Hash>] The values of all the options.
|
38
|
+
def values
|
39
|
+
options.each_key.to_h { |name| [name, get_value(name)] }
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# Gets the value of an option.
|
45
|
+
#
|
46
|
+
# If the option is not set, it will return the default value.
|
47
|
+
#
|
48
|
+
# @param name [Symbol] The name of the option.
|
49
|
+
# @return [Object] The value of the option.
|
50
|
+
#
|
51
|
+
# @raise [InvalidOptionError] If the option is not defined.
|
52
|
+
def get_value(name)
|
53
|
+
raise InvalidOptionError, "Option #{name} is not defined" unless options.key?(name)
|
54
|
+
|
55
|
+
@params.fetch(name, options[name]&.[](:default))
|
56
|
+
end
|
57
|
+
|
58
|
+
# Sets the value of an option.
|
59
|
+
#
|
60
|
+
# @param name [Symbol] The name of the option.
|
61
|
+
# @param value [Object] The value to set.
|
62
|
+
# @return [Object] The value that was set for the option.
|
63
|
+
#
|
64
|
+
# @raise [InvalidOptionError] If the option is not defined.
|
65
|
+
def set_value(name, value)
|
66
|
+
raise InvalidOptionError, "Option #{name} is not defined" unless options.key?(name)
|
67
|
+
|
68
|
+
@params ||= {}
|
69
|
+
@params[name] = value
|
70
|
+
end
|
71
|
+
|
72
|
+
# Required class methods for the config DSL.
|
73
|
+
module ClassMethods
|
74
|
+
# Define an option for the class.
|
75
|
+
#
|
76
|
+
# The keys for the options hash will vary depending on the modules that
|
77
|
+
# have been included in your class.
|
78
|
+
#
|
79
|
+
# @param name [Symbol] The name of the option.
|
80
|
+
# @param opts [Hash] The options for the option.
|
81
|
+
# @return [void]
|
82
|
+
def option(name, opts = {})
|
83
|
+
options.merge!({ name => opts })
|
84
|
+
define_method(name) { get_value(name) }
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns the options hash for the class.
|
88
|
+
#
|
89
|
+
# @return [Hash] The options hash.
|
90
|
+
def options
|
91
|
+
@options ||= {}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ConfigSL
|
4
|
+
module FileFormat
|
5
|
+
# Base class for file format support.
|
6
|
+
#
|
7
|
+
# @abstract Subclass and override `#read` and `.extensions` to implement a
|
8
|
+
# file format.
|
9
|
+
class Base
|
10
|
+
def initialize(file)
|
11
|
+
@file = file
|
12
|
+
end
|
13
|
+
|
14
|
+
# Extensions used to identify the file format.
|
15
|
+
#
|
16
|
+
# Values should be returned in order of preference.
|
17
|
+
#
|
18
|
+
# @return [Array<Symbol>] The extensions used to identify the file format.
|
19
|
+
def self.extensions
|
20
|
+
[]
|
21
|
+
end
|
22
|
+
|
23
|
+
# Reads the file and returns a hash of configuration values.
|
24
|
+
#
|
25
|
+
# @return [Hash{Symbol => Object}]
|
26
|
+
#
|
27
|
+
# @raise [NotImplementedError] If not implemented.
|
28
|
+
def read
|
29
|
+
raise NotImplementedError, 'Not implemented'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
require_relative 'base'
|
6
|
+
|
7
|
+
module ConfigSL
|
8
|
+
module FileFormat
|
9
|
+
# Support for JSON files.
|
10
|
+
class Json < Base
|
11
|
+
def self.extensions
|
12
|
+
%i[json]
|
13
|
+
end
|
14
|
+
|
15
|
+
def read
|
16
|
+
::JSON.parse(File.read(@file), symbolize_names: true).each do |name, value|
|
17
|
+
yield name, value if block_given?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
require_relative 'base'
|
6
|
+
|
7
|
+
module ConfigSL
|
8
|
+
module FileFormat
|
9
|
+
# Support for YAML files.
|
10
|
+
class Yaml < Base
|
11
|
+
def self.extensions
|
12
|
+
%i[yaml yml]
|
13
|
+
end
|
14
|
+
|
15
|
+
def read
|
16
|
+
::YAML.load_file(@file, symbolize_names: true).each do |name, value|
|
17
|
+
yield name, value if block_given?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ConfigSL
|
4
|
+
class FileFormatError < ArgumentError; end
|
5
|
+
class FileNotFoundError < ArgumentError; end
|
6
|
+
|
7
|
+
# Support for common file operations.
|
8
|
+
#
|
9
|
+
# This module provides limited functionality itself, but instead provides
|
10
|
+
# basic support for configuration files that is leveraged by other modules.
|
11
|
+
#
|
12
|
+
# Including this modules adds the following DSL methods to your class:
|
13
|
+
#
|
14
|
+
# - config_file_path: Set the default path to look for configuration files
|
15
|
+
# - config_file_name: Set the default file name to look for configuration
|
16
|
+
# files, without extension
|
17
|
+
# - register_file_format: Add support for a file format
|
18
|
+
module FileSupport
|
19
|
+
def self.included(base)
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Required class methods to support file operations.
|
24
|
+
module ClassMethods
|
25
|
+
# The default name to use for the configuration file.
|
26
|
+
#
|
27
|
+
# @param filename [String] The name of the configuration file.
|
28
|
+
# @return [String] The name of the configuration file.
|
29
|
+
def config_file_name(filename = nil)
|
30
|
+
@config_file_name = filename unless filename.nil?
|
31
|
+
@config_file_name ||= 'config'
|
32
|
+
end
|
33
|
+
|
34
|
+
# The default path to use for the configuration file.
|
35
|
+
#
|
36
|
+
# @param path [String] The path to the configuration file.
|
37
|
+
# @return [String] The path to the configuration file.
|
38
|
+
def config_file_path(path = nil)
|
39
|
+
@config_file_path = path unless path.nil?
|
40
|
+
@config_file_path ||= '.'
|
41
|
+
end
|
42
|
+
|
43
|
+
# The file extensions supported by this configuration.
|
44
|
+
#
|
45
|
+
# @param format [Symbol] Optional format to get the extensions for. If
|
46
|
+
# specified, only returns the extensions for that format.
|
47
|
+
# @return [Array<String>]
|
48
|
+
def file_extensions(format: nil)
|
49
|
+
@config_file_formats ||= {}
|
50
|
+
if format
|
51
|
+
return @config_file_formats[format][:extensions] if @config_file_formats.key?(format)
|
52
|
+
|
53
|
+
raise FileFormatError, "File format not found: #{format}"
|
54
|
+
end
|
55
|
+
|
56
|
+
@config_file_formats.values.map { |o| o[:extensions] }.flatten
|
57
|
+
end
|
58
|
+
|
59
|
+
# Register support for a file format.
|
60
|
+
#
|
61
|
+
# @param format [Symbol] The format to register.
|
62
|
+
# @param opts [Hash] The options for the format.
|
63
|
+
# @option opts [Class] :class The class to use for the format. Defaults to
|
64
|
+
# ConfigSL::FileFormat::`format`, where `format` is capitalized.
|
65
|
+
# @option opts [Array<String>] :extensions File extensions for the format.
|
66
|
+
# defaults to those defined in `opts[:class]`.
|
67
|
+
def register_file_format(format, opts = {})
|
68
|
+
@config_file_formats ||= {}
|
69
|
+
opts[:class] ||= ConfigSL::FileFormat.const_get(format.capitalize)
|
70
|
+
opts[:extensions] ||= opts[:class].extensions
|
71
|
+
|
72
|
+
@config_file_formats[format] = opts
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# Find the configuration file based on the default path, name, and defined
|
78
|
+
# formats.
|
79
|
+
#
|
80
|
+
# The returned files are ordered based on how their formats are defined.
|
81
|
+
# For example:
|
82
|
+
#
|
83
|
+
# register_file_format :yaml
|
84
|
+
# register_file_format :json
|
85
|
+
#
|
86
|
+
# would result in the YAML file being returned first if both are found.
|
87
|
+
#
|
88
|
+
# @param path [String] Optional path to look for the file. If provided,
|
89
|
+
# this method will raise an error the exact file is not found.
|
90
|
+
# @param format [Symbol] Optional format to look for the file in. If
|
91
|
+
# provided, will only match files of the given format. Ignored if `path`
|
92
|
+
# is provided.
|
93
|
+
# @return [Array<String>] Array of matching files.
|
94
|
+
def find_file(path: nil, format: nil)
|
95
|
+
paths = Dir.glob(
|
96
|
+
path.nil? ? "#{config_file_name}.{#{file_extensions(format:).join(',')}}" : path,
|
97
|
+
base: path.nil? ? config_file_path : nil
|
98
|
+
)
|
99
|
+
|
100
|
+
raise FileNotFoundError, 'No configuration file found!' if paths.empty?
|
101
|
+
|
102
|
+
paths.map { |p| File.join(config_file_path, p) }
|
103
|
+
end
|
104
|
+
|
105
|
+
# Find the format for a file given its extension.
|
106
|
+
#
|
107
|
+
# @param extension [String] The file extension to get the format for.
|
108
|
+
# @return [Symbol] The format for the file.
|
109
|
+
#
|
110
|
+
# @raise [FileFormatError] If no file formats have been defined.
|
111
|
+
# @raise [FileFormatError] If no file format is found for the extension.
|
112
|
+
def find_file_format(extension)
|
113
|
+
raise FileFormatError, 'No file formats have been defined' if @config_file_formats.nil?
|
114
|
+
|
115
|
+
extension = extension.sub(/^\./, '').to_sym
|
116
|
+
(@config_file_formats || {}).each do |format, opts|
|
117
|
+
return format if opts[:extensions].include?(extension)
|
118
|
+
end
|
119
|
+
|
120
|
+
raise FileFormatError, "No file format found for extension: #{extension}"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'facets/boolean'
|
4
|
+
|
5
|
+
module ConfigSL
|
6
|
+
class FormatError < ArgumentError; end
|
7
|
+
|
8
|
+
# Format option values.
|
9
|
+
#
|
10
|
+
# This will format the values of the defined options when they are set on the
|
11
|
+
# config object. It will also format the values when they are retrieved, if
|
12
|
+
# they don't match their defined type.
|
13
|
+
#
|
14
|
+
# The default behavior is to cast the value as the defined type. If the value
|
15
|
+
# is nil, it will be returned as is.
|
16
|
+
#
|
17
|
+
# @todo Should we add an option to cast nil?
|
18
|
+
module Format
|
19
|
+
FORMATTERS = {
|
20
|
+
Array => :to_a,
|
21
|
+
FalseClass => :to_b,
|
22
|
+
Hash => :to_h,
|
23
|
+
Integer => :to_i,
|
24
|
+
String => ->(v) { v.to_s.encode('utf-8') },
|
25
|
+
Symbol => :to_sym,
|
26
|
+
TrueClass => :to_b
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def get_value(name)
|
32
|
+
value = super
|
33
|
+
format_value(name, value)
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_value(name, value)
|
37
|
+
raise InvalidOptionError, "Option #{name} is not defined" unless options.key?(name)
|
38
|
+
|
39
|
+
super(name, format_value(name, value))
|
40
|
+
end
|
41
|
+
|
42
|
+
def format_value(option, value)
|
43
|
+
return value if value.nil? || value.is_a?(options[option][:type])
|
44
|
+
|
45
|
+
apply_formatter(value, options[option][:type])
|
46
|
+
rescue StandardError => e
|
47
|
+
raise FormatError, "Value for #{option} is not compatible with " \
|
48
|
+
"#{options[option][:type]}: #{e.message}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def apply_formatter(value, formatter)
|
52
|
+
if FORMATTERS.key?(formatter)
|
53
|
+
return value.send(FORMATTERS[formatter]) if FORMATTERS[formatter].is_a?(Symbol)
|
54
|
+
return FORMATTERS[formatter].call(value) if FORMATTERS[formatter].is_a?(Proc)
|
55
|
+
end
|
56
|
+
|
57
|
+
formatter.new(value)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ConfigSL
|
4
|
+
# Load configuration from the environment.
|
5
|
+
#
|
6
|
+
# This module provides a way to load configuration values from environment
|
7
|
+
# variables. It checks for a variables using the name of the option, in upper
|
8
|
+
# snake case (e.g. `MY_OPTION`). You can add a prefix to all variable names
|
9
|
+
# using `from_environment_prefix`.
|
10
|
+
#
|
11
|
+
# from_environment_prefix 'DATABASE_'
|
12
|
+
# option :host, default: 'localhost'
|
13
|
+
#
|
14
|
+
# You can override the variable name for individual options by setting
|
15
|
+
# `env_variable`.
|
16
|
+
#
|
17
|
+
# option :host, default: 'localhost', env_variable: 'DB_HOST'
|
18
|
+
module FromEnvironment
|
19
|
+
def self.included(base)
|
20
|
+
base.extend ClassMethods
|
21
|
+
base.from_environment_prefix ''
|
22
|
+
end
|
23
|
+
|
24
|
+
# Class methods necessary for loading configuration from the environment.
|
25
|
+
module ClassMethods
|
26
|
+
# Set the prefix for environment variables.
|
27
|
+
#
|
28
|
+
# @param prefix [String] The prefix for the environment variables.
|
29
|
+
def from_environment_prefix(prefix)
|
30
|
+
@from_environment_prefix = prefix
|
31
|
+
end
|
32
|
+
|
33
|
+
# Create a new instance of the class using values from the environment.
|
34
|
+
#
|
35
|
+
# @return [self] The new config object
|
36
|
+
def from_environment
|
37
|
+
params = options.transform_values do |opts|
|
38
|
+
ENV.fetch(opts[:env_variable], opts[:default])
|
39
|
+
end
|
40
|
+
|
41
|
+
new(params)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Ensure each option has an environment variable defined.
|
45
|
+
def option(name, opts = {})
|
46
|
+
opts[:env_variable] ||= "#{@from_environment_prefix}#{name.to_s.upcase}"
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'file_support'
|
4
|
+
|
5
|
+
module ConfigSL
|
6
|
+
# Load configuration from a file.
|
7
|
+
#
|
8
|
+
# This module provides a way to load configuration values from a file. It
|
9
|
+
# searches for files based on a default path, name, and one or more file
|
10
|
+
# formats.
|
11
|
+
#
|
12
|
+
# When multiple files are found, they will be sorted based on the order their
|
13
|
+
# formats are defined, and the first file will be loaded.
|
14
|
+
#
|
15
|
+
# For example:
|
16
|
+
#
|
17
|
+
# config_file_path 'config'
|
18
|
+
# config_file_name 'config'
|
19
|
+
#
|
20
|
+
# register_file_format :yaml
|
21
|
+
# register_file_format :json
|
22
|
+
#
|
23
|
+
# will search for files in the `config` directory, with the name `config`, and
|
24
|
+
# with the extensions `.yaml`, `.yml` and `.json`. If both `config.yaml` and
|
25
|
+
# `config.json` are found, `config.yaml` will be loaded.
|
26
|
+
#
|
27
|
+
# This module depends on the `FileSupport` module and will it include it your
|
28
|
+
# class if it has not been so already.
|
29
|
+
module FromFile
|
30
|
+
def self.included(base)
|
31
|
+
base.include(FileSupport) unless base.include?(FileSupport)
|
32
|
+
base.extend(ClassMethods)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Required class methods for loading config files.
|
36
|
+
module ClassMethods
|
37
|
+
# Loads configuration from a file.
|
38
|
+
#
|
39
|
+
# If no path is specified, uses the file file that matches the default
|
40
|
+
# path, name, and file formats. If multiple files are found, they will be
|
41
|
+
# sorted based on the order the file formats are defined, adn the first
|
42
|
+
# file will be load.
|
43
|
+
#
|
44
|
+
# @param path [String] Optional path to the file to load.
|
45
|
+
# @param format [Symbol] Optional format to use for the file. Uses the
|
46
|
+
# file extension if not specified.
|
47
|
+
# @return [self]
|
48
|
+
def from_file(path = nil, format: nil)
|
49
|
+
path ||= find_file.first
|
50
|
+
format ||= find_file_format(File.extname(path))
|
51
|
+
file = @config_file_formats[format][:class].new(path)
|
52
|
+
new(file.read)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ConfigSL
|
4
|
+
class ValidationError < RuntimeError; end
|
5
|
+
|
6
|
+
# Validates configuration options.
|
7
|
+
#
|
8
|
+
# This will check that all required options are set. Additionally, you can
|
9
|
+
# set `enum` to an array of valid values for an option.
|
10
|
+
#
|
11
|
+
# option :state, type: String, required: true, enum: %w[on off]
|
12
|
+
#
|
13
|
+
# @todo Implement custom validations.
|
14
|
+
module Validation
|
15
|
+
# Determine if the configuration is valid.
|
16
|
+
#
|
17
|
+
# @return [Boolean]
|
18
|
+
def valid?
|
19
|
+
options.keys.all? { |name| option_valid?(name) }
|
20
|
+
end
|
21
|
+
|
22
|
+
# Validate the configuration and raise an error if it is invalid.
|
23
|
+
#
|
24
|
+
# @raise [ValidationError] If the configuration is invalid.
|
25
|
+
def validate!
|
26
|
+
raise ValidationError, 'Invalid configuration' unless valid?
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Validates a single option.
|
32
|
+
#
|
33
|
+
# @param name [Symbol] The name of the option.
|
34
|
+
# @return [Boolean]
|
35
|
+
def option_valid?(name)
|
36
|
+
valid = !options[name][:required] || get_value(name)
|
37
|
+
valid &&= options[name][:enum].include?(get_value(name)) if options[name][:enum]
|
38
|
+
|
39
|
+
valid
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/configsl.rb
ADDED
metadata
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: configsl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James I. Armes
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-09-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: facets
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '3.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '3.1'
|
27
|
+
description: A simple, modular, extensible DSL for configuration.
|
28
|
+
email: jamesiarmes@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files:
|
32
|
+
- README.md
|
33
|
+
- CHANGELOG.md
|
34
|
+
files:
|
35
|
+
- CHANGELOG.md
|
36
|
+
- Gemfile
|
37
|
+
- README.md
|
38
|
+
- lib/configsl.rb
|
39
|
+
- lib/configsl/config.rb
|
40
|
+
- lib/configsl/dsl.rb
|
41
|
+
- lib/configsl/exception.rb
|
42
|
+
- lib/configsl/file_format/base.rb
|
43
|
+
- lib/configsl/file_format/json.rb
|
44
|
+
- lib/configsl/file_format/yaml.rb
|
45
|
+
- lib/configsl/file_support.rb
|
46
|
+
- lib/configsl/format.rb
|
47
|
+
- lib/configsl/from_environment.rb
|
48
|
+
- lib/configsl/from_file.rb
|
49
|
+
- lib/configsl/validation.rb
|
50
|
+
homepage: https://github.com/jamesiarmes/configsl
|
51
|
+
licenses:
|
52
|
+
- MIT
|
53
|
+
metadata:
|
54
|
+
bug_tracker_uri: https://github.com/jamesiarmes/configsl/issues
|
55
|
+
changelog_uri: https://github.com/jamesiarmes/configsl/blob/main/CHANGELOG.md
|
56
|
+
homepage_uri: https://github.com/jamesiarmes/configsl
|
57
|
+
rubygems_mfa_required: 'true'
|
58
|
+
source_code_uri: https://github.com/jamesiarmes/configsl
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3.2'
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
requirements: []
|
74
|
+
rubygems_version: 3.5.9
|
75
|
+
signing_key:
|
76
|
+
specification_version: 4
|
77
|
+
summary: A simple DSL for declarative configuration in ruby.
|
78
|
+
test_files: []
|