shinsei_config 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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +46 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +171 -0
- data/Rakefile +12 -0
- data/lib/shinsei_config/config_builder.rb +50 -0
- data/lib/shinsei_config/loader.rb +55 -0
- data/lib/shinsei_config/root_config.rb +90 -0
- data/lib/shinsei_config/schema_validator.rb +27 -0
- data/lib/shinsei_config/settings.rb +27 -0
- data/lib/shinsei_config/validation_error.rb +30 -0
- data/lib/shinsei_config/version.rb +5 -0
- data/lib/shinsei_config.rb +14 -0
- data/manifest.yaml +2 -0
- data/sig/shinsei_config/config_builder.rbs +25 -0
- data/sig/shinsei_config/loader.rbs +38 -0
- data/sig/shinsei_config/root_config.rbs +42 -0
- data/sig/shinsei_config/schema_validator.rbs +17 -0
- data/sig/shinsei_config/settings.rbs +33 -0
- data/sig/shinsei_config/validation_error.rbs +19 -0
- data/sig/shinsei_config/version.rbs +5 -0
- data/sig/shinsei_config.rbs +6 -0
- metadata +86 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6af621e538e6ba8146f4ba7814770563595b1c522baa0010055e6c93d17641f0
|
|
4
|
+
data.tar.gz: b79735e216a52f3a899b4c395dda017df9da3cd5b829ea329937baf2857ce234
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d0e96f8135a98c3c61781c75a25e6af63d43a7ad67c6e7055b06933181149cbd9db7709f6a74cb305580c24c422003d41840514c81de61a8d4b414519278fc82
|
|
7
|
+
data.tar.gz: 718a2a75ffc48008cbf3726417a54c90304958f8264952524dab6af375bac0254cc657046417328cebfe459e7d9a09e6b15058183e5ae043be6e594be13c92d8
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-rake
|
|
3
|
+
- rubocop-rspec
|
|
4
|
+
|
|
5
|
+
AllCops:
|
|
6
|
+
TargetRubyVersion: 3.3
|
|
7
|
+
NewCops: enable
|
|
8
|
+
|
|
9
|
+
Style/StringLiterals:
|
|
10
|
+
EnforcedStyle: single_quotes
|
|
11
|
+
|
|
12
|
+
Style/StringLiteralsInInterpolation:
|
|
13
|
+
EnforcedStyle: single_quotes
|
|
14
|
+
|
|
15
|
+
Metrics/MethodLength:
|
|
16
|
+
Max: 20
|
|
17
|
+
Exclude:
|
|
18
|
+
- 'spec/**/*.rb'
|
|
19
|
+
|
|
20
|
+
Metrics/BlockLength:
|
|
21
|
+
Exclude:
|
|
22
|
+
- 'spec/**/*.rb'
|
|
23
|
+
|
|
24
|
+
Metrics/ClassLength:
|
|
25
|
+
Max: 200
|
|
26
|
+
|
|
27
|
+
Metrics/ParameterLists:
|
|
28
|
+
CountKeywordArgs: false
|
|
29
|
+
|
|
30
|
+
Layout/LeadingCommentSpace:
|
|
31
|
+
AllowRBSInlineAnnotation: true
|
|
32
|
+
|
|
33
|
+
RSpec/NamedSubject:
|
|
34
|
+
Enabled: false
|
|
35
|
+
|
|
36
|
+
RSpec/MultipleExpectations:
|
|
37
|
+
Enabled: false
|
|
38
|
+
|
|
39
|
+
RSpec/MultipleMemoizedHelpers:
|
|
40
|
+
Enabled: false
|
|
41
|
+
|
|
42
|
+
RSpec/NestedGroups:
|
|
43
|
+
Max: 4
|
|
44
|
+
|
|
45
|
+
RSpec/ExampleLength:
|
|
46
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Denis Talakevich
|
|
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,171 @@
|
|
|
1
|
+
# ShinseiConfig
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/shinsei_config)
|
|
4
|
+
[](https://github.com/senid231/shinsei_config/actions/workflows/main.yml)
|
|
5
|
+
[](https://github.com/senid231/shinsei_config/actions/workflows/github-code-scanning/codeql)
|
|
6
|
+
|
|
7
|
+
Shinsei (真正 — "genuine, correct, authentic")
|
|
8
|
+
|
|
9
|
+
A Ruby gem for loading configuration from YAML files with schema validation, ERB interpolation, and environment-specific settings.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Schema Validation**: Uses [dry-schema](https://dry-rb.org/gems/dry-schema/) for robust configuration validation
|
|
14
|
+
- **ERB Interpolation**: Process ERB templates before YAML parsing
|
|
15
|
+
- **Environment Splitting**: Load environment-specific configurations
|
|
16
|
+
- **Immutable Config Objects**: Uses Ruby 3.2+ `Data.define` for frozen, immutable configuration objects
|
|
17
|
+
- **Type-Safe Access**: Access configuration values with dot notation
|
|
18
|
+
- **Custom YAML Options**: Pass custom options to YAML parser
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
bundle add shinsei_config
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
gem install shinsei_config
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
### Basic Example
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require 'shinsei_config'
|
|
40
|
+
|
|
41
|
+
class MyAppConfig < ShinseiConfig::RootConfig
|
|
42
|
+
settings do |s|
|
|
43
|
+
s.config_path = Rails.root.join('config/my_app_config.yml').to_s # required
|
|
44
|
+
s.env = Rails.env.to_s # optional, default nil which means no env splitting
|
|
45
|
+
s.yaml_opts = { aliases: true } # optional, default {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
schema do
|
|
49
|
+
required(:write_account_stats).value(:bool?)
|
|
50
|
+
required(:api_keys).hash do
|
|
51
|
+
required(:service_a).filled(:string)
|
|
52
|
+
required(:service_b).filled(:string)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.service_a_test?
|
|
57
|
+
# custom methods to access config values
|
|
58
|
+
api_keys.service_a.start_with?('test-')
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# When application starts
|
|
63
|
+
MyAppConfig.load! # may raise ShinseiConfig::ValidationError if config is invalid
|
|
64
|
+
|
|
65
|
+
# Somewhere in the app
|
|
66
|
+
MyAppConfig.api_keys.service_a # => "actual-key"
|
|
67
|
+
MyAppConfig.write_account_stats # => true
|
|
68
|
+
MyAppConfig.service_a_test? # => false
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### YAML File Examples
|
|
72
|
+
|
|
73
|
+
#### Simple Configuration (config/my_app_config.yml)
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
write_account_stats: true
|
|
77
|
+
api_keys:
|
|
78
|
+
service_a: <%= ENV['SERVICE_A_KEY'] %>
|
|
79
|
+
service_b: production-key-b
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### Environment-Specific Configuration
|
|
83
|
+
|
|
84
|
+
```yaml
|
|
85
|
+
development:
|
|
86
|
+
write_account_stats: false
|
|
87
|
+
api_keys:
|
|
88
|
+
service_a: test-key-a
|
|
89
|
+
service_b: test-key-b
|
|
90
|
+
|
|
91
|
+
production:
|
|
92
|
+
write_account_stats: true
|
|
93
|
+
api_keys:
|
|
94
|
+
service_a: <%= ENV['SERVICE_A_KEY'] %>
|
|
95
|
+
service_b: <%= ENV['SERVICE_B_KEY'] %>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Features in Detail
|
|
99
|
+
|
|
100
|
+
#### ERB Interpolation
|
|
101
|
+
|
|
102
|
+
ERB is processed on the raw YAML content before parsing:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
# config.yml
|
|
106
|
+
database_url: <%= ENV.fetch('DATABASE_URL', 'postgres://localhost/mydb') %>
|
|
107
|
+
max_connections: <%= 2 * 5 %>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Schema Validation
|
|
111
|
+
|
|
112
|
+
The gem uses [dry-schema](https://dry-rb.org/gems/dry-schema/) for validation:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
schema do
|
|
116
|
+
required(:database).hash do
|
|
117
|
+
required(:host).filled(:string)
|
|
118
|
+
required(:port).filled(:integer)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
optional(:features).array(:string)
|
|
122
|
+
|
|
123
|
+
required(:timeout).value(:integer, gt?: 0)
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
#### Custom YAML Options
|
|
128
|
+
|
|
129
|
+
Pass custom options to the YAML parser:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
settings do |s|
|
|
133
|
+
s.config_path = 'config/app.yml'
|
|
134
|
+
s.yaml_opts = {
|
|
135
|
+
aliases: true,
|
|
136
|
+
permitted_classes: [Date, Time],
|
|
137
|
+
permitted_symbols: []
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Immutable Configuration
|
|
143
|
+
|
|
144
|
+
All configuration objects are deeply frozen and immutable:
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
MyAppConfig.api_keys.frozen? # => true
|
|
148
|
+
MyAppConfig.api_keys.service_a.frozen? # => true
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
#### Reloading Configuration
|
|
152
|
+
|
|
153
|
+
You can reload the configuration at runtime (useful for development):
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
MyAppConfig.reload!
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
164
|
+
|
|
165
|
+
## Contributing
|
|
166
|
+
|
|
167
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/senid231/shinsei_config.
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShinseiConfig
|
|
4
|
+
# Recursively builds Data.define objects from hash with deep freezing
|
|
5
|
+
class ConfigBuilder
|
|
6
|
+
# @rbs hash: Hash[untyped, untyped] -- return: Data
|
|
7
|
+
def self.build(hash)
|
|
8
|
+
new(hash).build
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# @rbs @hash: Hash[untyped, untyped]
|
|
12
|
+
|
|
13
|
+
# @rbs attr_reader hash: Hash[untyped, untyped]
|
|
14
|
+
attr_reader :hash
|
|
15
|
+
|
|
16
|
+
# @rbs hash: Hash[untyped, untyped] -- return: void
|
|
17
|
+
def initialize(hash)
|
|
18
|
+
@hash = hash
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @rbs return: Data
|
|
22
|
+
def build
|
|
23
|
+
raise Error, "Expected Hash, got #{hash.class}" unless hash.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
# Create a Data.define class with all keys as symbols
|
|
26
|
+
config_class = Data.define(*hash.keys.map(&:to_sym))
|
|
27
|
+
|
|
28
|
+
# Build values, recursively converting nested hashes
|
|
29
|
+
# Convert keys to symbols for Data.define
|
|
30
|
+
values = hash.transform_keys(&:to_sym).transform_values { |v| build_value(v) }
|
|
31
|
+
|
|
32
|
+
# Create instance and freeze
|
|
33
|
+
config_class.new(**values).freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# @rbs value: untyped -- return: untyped
|
|
39
|
+
def build_value(value)
|
|
40
|
+
case value
|
|
41
|
+
when Hash
|
|
42
|
+
ConfigBuilder.build(value)
|
|
43
|
+
when Array
|
|
44
|
+
value.map { |v| build_value(v) }.freeze
|
|
45
|
+
else
|
|
46
|
+
value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'erb'
|
|
5
|
+
|
|
6
|
+
module ShinseiConfig
|
|
7
|
+
# Handles loading YAML files with ERB interpolation and environment splitting
|
|
8
|
+
class Loader
|
|
9
|
+
# @rbs @config_path: String
|
|
10
|
+
# @rbs @env: String?
|
|
11
|
+
# @rbs @yaml_opts: Hash[Symbol, untyped]
|
|
12
|
+
|
|
13
|
+
# @rbs attr_reader config_path: String
|
|
14
|
+
# @rbs attr_reader env: String?
|
|
15
|
+
# @rbs attr_reader yaml_opts: Hash[Symbol, untyped]
|
|
16
|
+
attr_reader :config_path, :env, :yaml_opts
|
|
17
|
+
|
|
18
|
+
# @rbs config_path: String, env: String?, **yaml_opts: untyped -- return: void
|
|
19
|
+
def initialize(config_path:, env: nil, **yaml_opts)
|
|
20
|
+
@config_path = config_path
|
|
21
|
+
@env = env
|
|
22
|
+
@yaml_opts = yaml_opts
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @rbs return: Hash[String, untyped]
|
|
26
|
+
def load
|
|
27
|
+
raise Error, "Config file not found: #{config_path}" unless File.exist?(config_path)
|
|
28
|
+
|
|
29
|
+
# Read raw content
|
|
30
|
+
raw_content = File.read(config_path)
|
|
31
|
+
|
|
32
|
+
# Process ERB on raw content before YAML parsing
|
|
33
|
+
erb_processed = ERB.new(raw_content).result
|
|
34
|
+
|
|
35
|
+
# Parse YAML with provided options
|
|
36
|
+
parsed_yaml = YAML.safe_load(erb_processed, **yaml_opts)
|
|
37
|
+
|
|
38
|
+
# Extract environment section if env is set
|
|
39
|
+
extract_env_section(parsed_yaml)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# @rbs parsed_yaml: Hash[String, untyped] -- return: Hash[String, untyped]
|
|
45
|
+
def extract_env_section(parsed_yaml)
|
|
46
|
+
return parsed_yaml unless env
|
|
47
|
+
|
|
48
|
+
unless parsed_yaml.is_a?(Hash) && parsed_yaml.key?(env)
|
|
49
|
+
raise Error, "Environment '#{env}' not found in config file"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
parsed_yaml[env]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShinseiConfig
|
|
4
|
+
# Base class for configuration. Users inherit from this class.
|
|
5
|
+
class RootConfig
|
|
6
|
+
class << self
|
|
7
|
+
# @rbs @settings: Settings?
|
|
8
|
+
# @rbs @schema_validator: SchemaValidator?
|
|
9
|
+
# @rbs @config: Data?
|
|
10
|
+
|
|
11
|
+
# Configure settings for this config class
|
|
12
|
+
# @rbs () { (Settings) -> void } -> Settings
|
|
13
|
+
def settings(&block)
|
|
14
|
+
@settings ||= Settings.new
|
|
15
|
+
block&.call(@settings)
|
|
16
|
+
@settings
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Define validation schema
|
|
20
|
+
# @rbs () { () -> void } -> SchemaValidator?
|
|
21
|
+
def schema(&block)
|
|
22
|
+
@schema_validator = SchemaValidator.new(&block) if block
|
|
23
|
+
@schema_validator
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Load and validate configuration
|
|
27
|
+
# @rbs return: singleton(RootConfig)
|
|
28
|
+
def load!
|
|
29
|
+
validate_setup!
|
|
30
|
+
|
|
31
|
+
# Load YAML with ERB and env splitting
|
|
32
|
+
loader = Loader.new(
|
|
33
|
+
config_path: settings.config_path,
|
|
34
|
+
env: settings.env,
|
|
35
|
+
**settings.yaml_opts
|
|
36
|
+
)
|
|
37
|
+
config_hash = loader.load
|
|
38
|
+
|
|
39
|
+
# Validate against schema if defined
|
|
40
|
+
schema&.validate!(config_hash)
|
|
41
|
+
|
|
42
|
+
# Build config object
|
|
43
|
+
@config = ConfigBuilder.build(config_hash)
|
|
44
|
+
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Reload configuration
|
|
49
|
+
# @rbs return: singleton(RootConfig)
|
|
50
|
+
def reload!
|
|
51
|
+
@config = nil
|
|
52
|
+
load!
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Access config instance
|
|
56
|
+
# @rbs return: Data
|
|
57
|
+
def config
|
|
58
|
+
raise Error, "Config not loaded. Call #{name}.load! first" unless @config
|
|
59
|
+
|
|
60
|
+
@config
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Delegate method calls to config instance
|
|
64
|
+
# @rbs method_name: Symbol, *args: untyped, **kwargs: untyped -- return: untyped
|
|
65
|
+
def method_missing(method_name, *, &)
|
|
66
|
+
if config.respond_to?(method_name)
|
|
67
|
+
config.public_send(method_name, *, &)
|
|
68
|
+
else
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @rbs method_name: Symbol, include_private: bool -- return: bool
|
|
74
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
75
|
+
config.respond_to?(method_name) || super
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# @rbs return: void
|
|
81
|
+
def validate_setup!
|
|
82
|
+
settings.validate!
|
|
83
|
+
|
|
84
|
+
return if schema
|
|
85
|
+
|
|
86
|
+
raise Error, "Schema not defined. Call #{name}.schema { ... } in class body"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'dry-schema'
|
|
4
|
+
|
|
5
|
+
module ShinseiConfig
|
|
6
|
+
# Wraps dry-schema for validating configuration
|
|
7
|
+
class SchemaValidator
|
|
8
|
+
# @rbs @schema: Dry::Schema::Processor
|
|
9
|
+
|
|
10
|
+
# @rbs attr_reader schema: Dry::Schema::Processor
|
|
11
|
+
attr_reader :schema
|
|
12
|
+
|
|
13
|
+
# @rbs **opts: untyped -- return: void
|
|
14
|
+
def initialize(**, &)
|
|
15
|
+
@schema = Dry::Schema.define(processor_type: Dry::Schema::JSON, **, &)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @rbs config_hash: Hash[untyped, untyped] -- return: void
|
|
19
|
+
def validate!(config_hash)
|
|
20
|
+
result = schema.call(config_hash)
|
|
21
|
+
|
|
22
|
+
return if result.success?
|
|
23
|
+
|
|
24
|
+
raise ValidationError, result.errors
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShinseiConfig
|
|
4
|
+
# Stores settings for RootConfig that are not part of the schema
|
|
5
|
+
class Settings
|
|
6
|
+
# @rbs @config_path: String?
|
|
7
|
+
# @rbs @env: String?
|
|
8
|
+
# @rbs @yaml_opts: Hash[Symbol, untyped]
|
|
9
|
+
|
|
10
|
+
# @rbs attr_accessor config_path: String?
|
|
11
|
+
# @rbs attr_accessor env: String?
|
|
12
|
+
# @rbs attr_accessor yaml_opts: Hash[Symbol, untyped]
|
|
13
|
+
attr_accessor :config_path, :env, :yaml_opts
|
|
14
|
+
|
|
15
|
+
# @rbs return: void
|
|
16
|
+
def initialize
|
|
17
|
+
@config_path = nil
|
|
18
|
+
@env = nil
|
|
19
|
+
@yaml_opts = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs return: void
|
|
23
|
+
def validate!
|
|
24
|
+
raise Error, 'config_path is required' if config_path.nil? || config_path.empty?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShinseiConfig
|
|
4
|
+
# Raised when config validation fails
|
|
5
|
+
class ValidationError < Error
|
|
6
|
+
# @rbs @errors: Hash[Symbol, Array[String]]
|
|
7
|
+
|
|
8
|
+
# @rbs attr_reader errors: Hash[Symbol, Array[String]]
|
|
9
|
+
attr_reader :errors
|
|
10
|
+
|
|
11
|
+
# @rbs errors: Hash[Symbol, Array[String]] -- return: void
|
|
12
|
+
def initialize(errors)
|
|
13
|
+
@errors = errors
|
|
14
|
+
super(format_errors)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
# @rbs return: String
|
|
20
|
+
def format_errors
|
|
21
|
+
return 'Validation failed' if errors.empty?
|
|
22
|
+
|
|
23
|
+
formatted = errors.to_h.map do |key, messages|
|
|
24
|
+
" #{key}: #{messages.join(', ')}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
"Validation failed:\n#{formatted.join("\n")}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'shinsei_config/version'
|
|
4
|
+
|
|
5
|
+
module ShinseiConfig
|
|
6
|
+
class Error < StandardError; end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
require_relative 'shinsei_config/settings'
|
|
10
|
+
require_relative 'shinsei_config/validation_error'
|
|
11
|
+
require_relative 'shinsei_config/loader'
|
|
12
|
+
require_relative 'shinsei_config/schema_validator'
|
|
13
|
+
require_relative 'shinsei_config/config_builder'
|
|
14
|
+
require_relative 'shinsei_config/root_config'
|
data/manifest.yaml
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Generated from lib/shinsei_config/config_builder.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module ShinseiConfig
|
|
4
|
+
# Recursively builds Data.define objects from hash with deep freezing
|
|
5
|
+
class ConfigBuilder
|
|
6
|
+
# @rbs hash: Hash[untyped, untyped] -- return: Data
|
|
7
|
+
def self.build: (Hash[untyped, untyped] hash) -> untyped
|
|
8
|
+
|
|
9
|
+
@hash: Hash[untyped, untyped]
|
|
10
|
+
|
|
11
|
+
# @rbs attr_reader hash: Hash[untyped, untyped]
|
|
12
|
+
attr_reader hash: untyped
|
|
13
|
+
|
|
14
|
+
# @rbs hash: Hash[untyped, untyped] -- return: void
|
|
15
|
+
def initialize: (Hash[untyped, untyped] hash) -> untyped
|
|
16
|
+
|
|
17
|
+
# @rbs return: Data
|
|
18
|
+
def build: () -> Data
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# @rbs value: untyped -- return: untyped
|
|
23
|
+
def build_value: (untyped value) -> untyped
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Generated from lib/shinsei_config/loader.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module ShinseiConfig
|
|
4
|
+
# Handles loading YAML files with ERB interpolation and environment splitting
|
|
5
|
+
class Loader
|
|
6
|
+
@config_path: String
|
|
7
|
+
|
|
8
|
+
@env: String?
|
|
9
|
+
|
|
10
|
+
@yaml_opts: Hash[Symbol, untyped]
|
|
11
|
+
|
|
12
|
+
# @rbs attr_reader config_path: String
|
|
13
|
+
# @rbs attr_reader env: String?
|
|
14
|
+
# @rbs attr_reader yaml_opts: Hash[Symbol, untyped]
|
|
15
|
+
attr_reader config_path: untyped
|
|
16
|
+
|
|
17
|
+
# @rbs attr_reader config_path: String
|
|
18
|
+
# @rbs attr_reader env: String?
|
|
19
|
+
# @rbs attr_reader yaml_opts: Hash[Symbol, untyped]
|
|
20
|
+
attr_reader env: untyped
|
|
21
|
+
|
|
22
|
+
# @rbs attr_reader config_path: String
|
|
23
|
+
# @rbs attr_reader env: String?
|
|
24
|
+
# @rbs attr_reader yaml_opts: Hash[Symbol, untyped]
|
|
25
|
+
attr_reader yaml_opts: untyped
|
|
26
|
+
|
|
27
|
+
# @rbs config_path: String, env: String?, **yaml_opts: untyped -- return: void
|
|
28
|
+
def initialize: (config_path: String, ?env: untyped, **untyped yaml_opts) -> untyped
|
|
29
|
+
|
|
30
|
+
# @rbs return: Hash[String, untyped]
|
|
31
|
+
def load: () -> Hash[String, untyped]
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# @rbs parsed_yaml: Hash[String, untyped] -- return: Hash[String, untyped]
|
|
36
|
+
def extract_env_section: (Hash[String, untyped] parsed_yaml) -> untyped
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Generated from lib/shinsei_config/root_config.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module ShinseiConfig
|
|
4
|
+
# Base class for configuration. Users inherit from this class.
|
|
5
|
+
class RootConfig
|
|
6
|
+
@settings: Settings?
|
|
7
|
+
|
|
8
|
+
@schema_validator: SchemaValidator?
|
|
9
|
+
|
|
10
|
+
@config: Data?
|
|
11
|
+
|
|
12
|
+
# Configure settings for this config class
|
|
13
|
+
# @rbs () { (Settings) -> void } -> Settings
|
|
14
|
+
def self.settings: () { (Settings) -> void } -> Settings
|
|
15
|
+
|
|
16
|
+
# Define validation schema
|
|
17
|
+
# @rbs () { () -> void } -> SchemaValidator?
|
|
18
|
+
def self.schema: () { () -> void } -> SchemaValidator?
|
|
19
|
+
|
|
20
|
+
# Load and validate configuration
|
|
21
|
+
# @rbs return: singleton(RootConfig)
|
|
22
|
+
def self.load!: () -> singleton(RootConfig)
|
|
23
|
+
|
|
24
|
+
# Reload configuration
|
|
25
|
+
# @rbs return: singleton(RootConfig)
|
|
26
|
+
def self.reload!: () -> singleton(RootConfig)
|
|
27
|
+
|
|
28
|
+
# Access config instance
|
|
29
|
+
# @rbs return: Data
|
|
30
|
+
def self.config: () -> Data
|
|
31
|
+
|
|
32
|
+
# Delegate method calls to config instance
|
|
33
|
+
# @rbs method_name: Symbol, *args: untyped, **kwargs: untyped -- return: untyped
|
|
34
|
+
def self.method_missing: (Symbol method_name, *untyped) ?{ (?) -> untyped } -> untyped
|
|
35
|
+
|
|
36
|
+
# @rbs method_name: Symbol, include_private: bool -- return: bool
|
|
37
|
+
def self.respond_to_missing?: (Symbol method_name, ?untyped include_private) -> untyped
|
|
38
|
+
|
|
39
|
+
# @rbs return: void
|
|
40
|
+
private def self.validate_setup!: () -> void
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated from lib/shinsei_config/schema_validator.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module ShinseiConfig
|
|
4
|
+
# Wraps dry-schema for validating configuration
|
|
5
|
+
class SchemaValidator
|
|
6
|
+
@schema: Dry::Schema::Processor
|
|
7
|
+
|
|
8
|
+
# @rbs attr_reader schema: Dry::Schema::Processor
|
|
9
|
+
attr_reader schema: untyped
|
|
10
|
+
|
|
11
|
+
# @rbs **opts: untyped -- return: void
|
|
12
|
+
def initialize: (**untyped) ?{ (?) -> untyped } -> untyped
|
|
13
|
+
|
|
14
|
+
# @rbs config_hash: Hash[untyped, untyped] -- return: void
|
|
15
|
+
def validate!: (Hash[untyped, untyped] config_hash) -> untyped
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Generated from lib/shinsei_config/settings.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module ShinseiConfig
|
|
4
|
+
# Stores settings for RootConfig that are not part of the schema
|
|
5
|
+
class Settings
|
|
6
|
+
@config_path: String?
|
|
7
|
+
|
|
8
|
+
@env: String?
|
|
9
|
+
|
|
10
|
+
@yaml_opts: Hash[Symbol, untyped]
|
|
11
|
+
|
|
12
|
+
# @rbs attr_accessor config_path: String?
|
|
13
|
+
# @rbs attr_accessor env: String?
|
|
14
|
+
# @rbs attr_accessor yaml_opts: Hash[Symbol, untyped]
|
|
15
|
+
attr_accessor config_path: untyped
|
|
16
|
+
|
|
17
|
+
# @rbs attr_accessor config_path: String?
|
|
18
|
+
# @rbs attr_accessor env: String?
|
|
19
|
+
# @rbs attr_accessor yaml_opts: Hash[Symbol, untyped]
|
|
20
|
+
attr_accessor env: untyped
|
|
21
|
+
|
|
22
|
+
# @rbs attr_accessor config_path: String?
|
|
23
|
+
# @rbs attr_accessor env: String?
|
|
24
|
+
# @rbs attr_accessor yaml_opts: Hash[Symbol, untyped]
|
|
25
|
+
attr_accessor yaml_opts: untyped
|
|
26
|
+
|
|
27
|
+
# @rbs return: void
|
|
28
|
+
def initialize: () -> void
|
|
29
|
+
|
|
30
|
+
# @rbs return: void
|
|
31
|
+
def validate!: () -> void
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Generated from lib/shinsei_config/validation_error.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module ShinseiConfig
|
|
4
|
+
# Raised when config validation fails
|
|
5
|
+
class ValidationError < Error
|
|
6
|
+
@errors: Hash[Symbol, Array[String]]
|
|
7
|
+
|
|
8
|
+
# @rbs attr_reader errors: Hash[Symbol, Array[String]]
|
|
9
|
+
attr_reader errors: untyped
|
|
10
|
+
|
|
11
|
+
# @rbs errors: Hash[Symbol, Array[String]] -- return: void
|
|
12
|
+
def initialize: (Hash[Symbol, Array[String]] errors) -> untyped
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# @rbs return: String
|
|
17
|
+
def format_errors: () -> String
|
|
18
|
+
end
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: shinsei_config
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Denis Talakevich
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-11-17 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: dry-schema
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.13'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.13'
|
|
27
|
+
description: ShinseiConfig (真正 — genuine, correct, authentic) loads configuration
|
|
28
|
+
from YAML files, supports ERB interpolation, per-environment splitting, and validates
|
|
29
|
+
structure using dry-schema.
|
|
30
|
+
email:
|
|
31
|
+
- senid231@gmail.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- ".rspec"
|
|
37
|
+
- ".rubocop.yml"
|
|
38
|
+
- CHANGELOG.md
|
|
39
|
+
- LICENSE.txt
|
|
40
|
+
- README.md
|
|
41
|
+
- Rakefile
|
|
42
|
+
- lib/shinsei_config.rb
|
|
43
|
+
- lib/shinsei_config/config_builder.rb
|
|
44
|
+
- lib/shinsei_config/loader.rb
|
|
45
|
+
- lib/shinsei_config/root_config.rb
|
|
46
|
+
- lib/shinsei_config/schema_validator.rb
|
|
47
|
+
- lib/shinsei_config/settings.rb
|
|
48
|
+
- lib/shinsei_config/validation_error.rb
|
|
49
|
+
- lib/shinsei_config/version.rb
|
|
50
|
+
- manifest.yaml
|
|
51
|
+
- sig/shinsei_config.rbs
|
|
52
|
+
- sig/shinsei_config/config_builder.rbs
|
|
53
|
+
- sig/shinsei_config/loader.rbs
|
|
54
|
+
- sig/shinsei_config/root_config.rbs
|
|
55
|
+
- sig/shinsei_config/schema_validator.rbs
|
|
56
|
+
- sig/shinsei_config/settings.rbs
|
|
57
|
+
- sig/shinsei_config/validation_error.rbs
|
|
58
|
+
- sig/shinsei_config/version.rbs
|
|
59
|
+
homepage: https://github.com/senid231/shinsei_config
|
|
60
|
+
licenses:
|
|
61
|
+
- MIT
|
|
62
|
+
metadata:
|
|
63
|
+
homepage_uri: https://github.com/senid231/shinsei_config
|
|
64
|
+
source_code_uri: https://github.com/senid231/shinsei_config
|
|
65
|
+
changelog_uri: https://github.com/senid231/shinsei_config/blob/master/CHANGELOG.md
|
|
66
|
+
rubygems_mfa_required: 'true'
|
|
67
|
+
post_install_message:
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: 3.3.0
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 3.5.22
|
|
83
|
+
signing_key:
|
|
84
|
+
specification_version: 4
|
|
85
|
+
summary: Loads and validates configuration from YAML files with schema validation
|
|
86
|
+
test_files: []
|