hanami-settings-stores 0.1.0 → 0.1.2
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 +4 -4
- data/CHANGELOG.md +34 -2
- data/lib/hanami/settings/file_store.rb +161 -0
- metadata +14 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d683c61551360b47b9c1bdc99da1fa606f37be204668adc148d8b7a65d8ba97e
|
|
4
|
+
data.tar.gz: 62fc0ef126478624dd45a1bdd7b4aedb11f784cdbe6ea2700f0af0476e7cb60b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4ef346c826fbfa6f303c723f4d63bcab1350e9de61e6b5a0bde75862194c25b1c747c81b9920c6831629a44147f3d84cdad76b6aaafde9b7f2129d0a5cb70e3f
|
|
7
|
+
data.tar.gz: 86f6bdfa99ae003eb16e0c08a44bdc19f18377afe54fc6e383629ea98ca1de8e7847ad0c4e1d4e758ad686b698053ad258b9f4d2e0b5213af0700a550f42ef22
|
data/CHANGELOG.md
CHANGED
|
@@ -7,8 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## v0.1.
|
|
10
|
+
## [v0.1.2] - 2026-11-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `FileStore.new` accepts a block returning the path, resolved lazily on the first fetch
|
|
15
|
+
- `FileStore#path`, which resolves and memoizes the path
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Moved the project and CI to [Forgejo].
|
|
20
|
+
- `FileStore` parses YAML with `Date`, `DateTime`, `Symbol`, and `Time` permitted, so files
|
|
21
|
+
containing dates, times, or symbols no longer raise `Psych::DisallowedClass`
|
|
22
|
+
- **Breaking:** `FileStore` keys are now symbols, so nested values come back as hashes with symbol
|
|
23
|
+
keys rather than string keys
|
|
24
|
+
- **Breaking:** `FileStore` reads the file on the first fetch rather than when the store is built
|
|
25
|
+
- **Breaking:** `FileStore` treats a missing file as an empty store instead of raising
|
|
26
|
+
`Errno::ENOENT`, and raises `TypeError` when the file parses to something other than a mapping
|
|
27
|
+
|
|
28
|
+
## [v0.1.1] - 2026-08-20
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `FileStore` with json, toml, and yaml support
|
|
33
|
+
|
|
34
|
+
### Removed
|
|
35
|
+
|
|
36
|
+
- removed unnecessary `hanami` gem as a runtime dependency
|
|
37
|
+
|
|
38
|
+
## [v0.1.0] - 2026-03-17
|
|
11
39
|
|
|
12
40
|
Initial release.
|
|
13
41
|
|
|
14
|
-
[
|
|
42
|
+
[Forgejo]: https://aaronmallen.dev/aaron/hanami-settings-stores
|
|
43
|
+
[Unreleased]: https://aaronmallen.dev/aaron/hanami-settings-stores/compare/0.1.2...HEAD
|
|
44
|
+
[v0.1.2]: https://aaronmallen.dev/aaron/hanami-settings-stores/src/tag/0.1.2
|
|
45
|
+
[v0.1.1]: https://aaronmallen.dev/github-archive/hanami-settings-stores/compare/0.1.0...0.1.1
|
|
46
|
+
[v0.1.0]: https://aaronmallen.dev/github-archive/hanami-settings-stores/releases/tag/0.1.0
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require "erb"
|
|
5
|
+
|
|
6
|
+
module Hanami
|
|
7
|
+
class Settings
|
|
8
|
+
# A settings store backed by a local JSON, TOML, or YAML file.
|
|
9
|
+
#
|
|
10
|
+
# The file is read lazily, on the first fetch, and its contents are then memoized. Its contents
|
|
11
|
+
# run through ERB first, so a file can interpolate Ruby. A file that does not exist, or that is
|
|
12
|
+
# empty, results in an empty store.
|
|
13
|
+
#
|
|
14
|
+
# Only top level keys resolve. Keys are compared as symbols, so a setting named `:database_url`
|
|
15
|
+
# matches the `database_url` key in the file.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# # config/settings.yml
|
|
19
|
+
# database_url: postgres://localhost/myapp
|
|
20
|
+
# session_secret: <%= ENV.fetch("SESSION_SECRET") %>
|
|
21
|
+
#
|
|
22
|
+
# # config/app.rb
|
|
23
|
+
# config.settings_store = Hanami::Settings::CompositeStore.new(
|
|
24
|
+
# Hanami::Settings::EnvStore.new,
|
|
25
|
+
# Hanami::Settings::FileStore.new("config/settings.yml")
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# @example Resolving the path lazily
|
|
29
|
+
# # Give a block to resolve the path when the settings are first fetched, rather than when the
|
|
30
|
+
# # store is created.
|
|
31
|
+
# config.settings_store = Hanami::Settings::FileStore.new { config.root.join("config/settings.yml") }
|
|
32
|
+
#
|
|
33
|
+
# @api public
|
|
34
|
+
# @since 0.1.1
|
|
35
|
+
class FileStore
|
|
36
|
+
# @api private
|
|
37
|
+
# @since 0.2.0
|
|
38
|
+
EMPTY_SETTINGS = {}.freeze
|
|
39
|
+
private_constant :EMPTY_SETTINGS
|
|
40
|
+
|
|
41
|
+
# @api private
|
|
42
|
+
# @since 0.1.1
|
|
43
|
+
JSON_EXTENSIONS = %w[.json].freeze
|
|
44
|
+
private_constant :JSON_EXTENSIONS
|
|
45
|
+
|
|
46
|
+
# The classes permitted when parsing a YAML file.
|
|
47
|
+
#
|
|
48
|
+
# @api private
|
|
49
|
+
# @since 0.2.0
|
|
50
|
+
PERMITTED_CLASSES = [Date, DateTime, Symbol, Time].freeze
|
|
51
|
+
private_constant :PERMITTED_CLASSES
|
|
52
|
+
|
|
53
|
+
# @api private
|
|
54
|
+
# @since 0.1.1
|
|
55
|
+
TOML_EXTENSIONS = %w[.toml].freeze
|
|
56
|
+
private_constant :TOML_EXTENSIONS
|
|
57
|
+
|
|
58
|
+
# @api private
|
|
59
|
+
# @since 0.1.1
|
|
60
|
+
YAML_EXTENSIONS = %w[.yaml .yml].freeze
|
|
61
|
+
private_constant :YAML_EXTENSIONS
|
|
62
|
+
|
|
63
|
+
# @api private
|
|
64
|
+
# @since 0.1.1
|
|
65
|
+
EXTENSIONS = (JSON_EXTENSIONS + TOML_EXTENSIONS + YAML_EXTENSIONS).freeze
|
|
66
|
+
private_constant :EXTENSIONS
|
|
67
|
+
|
|
68
|
+
# Returns a new instance of the store.
|
|
69
|
+
#
|
|
70
|
+
# Give either a path or a block returning a path. The block is called on the first fetch,
|
|
71
|
+
# which allows the path to depend on config (such as the root) that is not yet known when the
|
|
72
|
+
# store is created.
|
|
73
|
+
#
|
|
74
|
+
# @param path [Pathname, String, nil] path to the settings file
|
|
75
|
+
#
|
|
76
|
+
# @yieldreturn [Pathname, String] path to the settings file
|
|
77
|
+
#
|
|
78
|
+
# @raise [ArgumentError] if given neither a path nor a block, or both
|
|
79
|
+
#
|
|
80
|
+
# @api public
|
|
81
|
+
# @since 0.1.1
|
|
82
|
+
def initialize(path = nil, &block)
|
|
83
|
+
raise ArgumentError, "give either a path or a block returning a path" if path.nil? == block.nil?
|
|
84
|
+
|
|
85
|
+
@path = path
|
|
86
|
+
@path_resolver = block
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Fetches a setting value from the parsed file.
|
|
90
|
+
#
|
|
91
|
+
# @param name [String, Symbol] the setting name
|
|
92
|
+
# @param args [Array] optional default value
|
|
93
|
+
# @yield [name] optional block for default value
|
|
94
|
+
# @return [Object] the setting value
|
|
95
|
+
# @raise [ArgumentError] if the store cannot parse the file extension
|
|
96
|
+
# @raise [KeyError] if the file has no such key and no default is given
|
|
97
|
+
# @raise [TypeError] if the file does not contain a mapping
|
|
98
|
+
#
|
|
99
|
+
# @api public
|
|
100
|
+
# @since 0.1.1
|
|
101
|
+
def fetch(name, *args, &block)
|
|
102
|
+
settings.fetch(name.to_sym, *args, &block)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns the path to the settings file, resolving it from the block given at initialize-time,
|
|
106
|
+
# if any.
|
|
107
|
+
#
|
|
108
|
+
# @return [Pathname, String]
|
|
109
|
+
#
|
|
110
|
+
# @api public
|
|
111
|
+
# @since 0.2.0
|
|
112
|
+
def path
|
|
113
|
+
@path ||= @path_resolver.call
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
def load_settings
|
|
119
|
+
return EMPTY_SETTINGS unless File.exist?(path)
|
|
120
|
+
|
|
121
|
+
contents = parse(ERB.new(File.read(path)).result)
|
|
122
|
+
|
|
123
|
+
# An empty file (or one containing only comments) parses as `nil`.
|
|
124
|
+
return EMPTY_SETTINGS if contents.nil?
|
|
125
|
+
|
|
126
|
+
unless contents.is_a?(Hash)
|
|
127
|
+
raise TypeError, "cannot parse #{path}: expected a mapping, got #{contents.class}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
contents.freeze
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def parse(source)
|
|
134
|
+
case File.extname(path.to_s)
|
|
135
|
+
when *JSON_EXTENSIONS
|
|
136
|
+
require "json"
|
|
137
|
+
JSON.parse(source, symbolize_names: true)
|
|
138
|
+
when *TOML_EXTENSIONS
|
|
139
|
+
require "toml"
|
|
140
|
+
symbolize(TOML::Parser.new(source).parsed)
|
|
141
|
+
when *YAML_EXTENSIONS
|
|
142
|
+
require "yaml"
|
|
143
|
+
YAML.load(source, aliases: true, permitted_classes: PERMITTED_CLASSES, symbolize_names: true)
|
|
144
|
+
else raise ArgumentError, "cannot parse #{path}: expected one of #{EXTENSIONS.join(", ")}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def settings
|
|
149
|
+
@settings ||= load_settings
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def symbolize(value)
|
|
153
|
+
case value
|
|
154
|
+
when Hash then value.to_h { |key, val| [key.to_sym, symbolize(val)] }
|
|
155
|
+
when Array then value.map { |val| symbolize(val) }
|
|
156
|
+
else value
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
metadata
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hanami-settings-stores
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aaron Allen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-11 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
|
-
name:
|
|
14
|
+
name: toml
|
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '
|
|
19
|
+
version: '0.3'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '
|
|
26
|
+
version: '0.3'
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: zeitwerk
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -38,8 +38,8 @@ dependencies:
|
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
40
|
version: '2'
|
|
41
|
-
description:
|
|
42
|
-
|
|
41
|
+
description: Settings stores that fetch values from password managers like 1Password
|
|
42
|
+
and Bitwarden, or from a local JSON, TOML, or YAML file, for use with Hanami::Settings.
|
|
43
43
|
email:
|
|
44
44
|
- hello@aaronmallen.me
|
|
45
45
|
executables: []
|
|
@@ -51,16 +51,17 @@ files:
|
|
|
51
51
|
- lib/hanami-settings-stores.rb
|
|
52
52
|
- lib/hanami/settings/bitwarden_store.rb
|
|
53
53
|
- lib/hanami/settings/composite_store.rb
|
|
54
|
+
- lib/hanami/settings/file_store.rb
|
|
54
55
|
- lib/hanami/settings/one_password_store.rb
|
|
55
|
-
homepage: https://
|
|
56
|
+
homepage: https://aaronmallen.dev/aaron/hanami-settings-stores
|
|
56
57
|
licenses:
|
|
57
58
|
- MIT
|
|
58
59
|
metadata:
|
|
59
|
-
bug_tracker_uri: https://
|
|
60
|
-
changelog_uri: https://
|
|
61
|
-
homepage_uri: https://
|
|
60
|
+
bug_tracker_uri: https://aaronmallen.dev/aaron/hanami-settings-stores/issues
|
|
61
|
+
changelog_uri: https://aaronmallen.dev/aaron/hanami-settings-stores/src/branch/main/CHANGELOG.md
|
|
62
|
+
homepage_uri: https://aaronmallen.dev/aaron/hanami-settings-stores
|
|
62
63
|
rubygems_mfa_required: 'true'
|
|
63
|
-
source_code_uri: https://
|
|
64
|
+
source_code_uri: https://aaronmallen.dev/aaron/hanami-settings-stores
|
|
64
65
|
post_install_message:
|
|
65
66
|
rdoc_options: []
|
|
66
67
|
require_paths:
|
|
@@ -79,5 +80,5 @@ requirements: []
|
|
|
79
80
|
rubygems_version: 3.4.19
|
|
80
81
|
signing_key:
|
|
81
82
|
specification_version: 4
|
|
82
|
-
summary:
|
|
83
|
+
summary: Pluggable settings stores for Hanami
|
|
83
84
|
test_files: []
|