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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c8228cf550f63d29462cb128f2949d9fa9a744aac843fbac90a811a5d550ec4
4
- data.tar.gz: f4abd1d91b57a4946712d602f1f37d1274d5ac89dc2e2e18eeb5f756877e4875
3
+ metadata.gz: d683c61551360b47b9c1bdc99da1fa606f37be204668adc148d8b7a65d8ba97e
4
+ data.tar.gz: 62fc0ef126478624dd45a1bdd7b4aedb11f784cdbe6ea2700f0af0476e7cb60b
5
5
  SHA512:
6
- metadata.gz: 6986d005390d907eadd3342e78da83016f495c91c48dbd7c53ecdb84d0aa3979ed459d2f5307445ae2e358fdae9db1ffa8cda6ddc02813cd199c61f39a81440f
7
- data.tar.gz: bf07691cc6721383c34c095f4c8122ee0fc6724e970c2084db25cea20e3fa93d579f751d19a81bfbd299642e28cb4d8b2fa9fc7cf849c0fd6db6ff716b795c6e
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.0 - 2026-03-17
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
- [Unreleased]: https://github.com/aaronmallen/hanami-settings-stores/compare/v0.1.0...HEAD
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.0
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-03-17 00:00:00.000000000 Z
11
+ date: 2026-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: hanami
14
+ name: toml
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '2'
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: '2'
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: Pluggable settings stores that fetch secrets from password managers like
42
- 1Password and Bitwarden for use with Hanami::Settings.
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://github.com/aaronmallen/hanami-settings-stores
56
+ homepage: https://aaronmallen.dev/aaron/hanami-settings-stores
56
57
  licenses:
57
58
  - MIT
58
59
  metadata:
59
- bug_tracker_uri: https://github.com/aaronmallen/hanami-settings-stores/issues
60
- changelog_uri: https://github.com/aaronmallen/hanami-settings-stores/blob/main/CHANGELOG.md
61
- homepage_uri: https://github.com/aaronmallen/hanami-settings-stores
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://github.com/aaronmallen/hanami-settings-stores
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: Password manager-backed settings stores for Hanami
83
+ summary: Pluggable settings stores for Hanami
83
84
  test_files: []