hanami-settings-stores 0.1.1 → 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: ce06f133c66bd28ca84dc455d158dea1037aafd6b09d5a3234a0c46f7ea4d7b3
4
- data.tar.gz: 6a1353b35214301a89a3a43ccd34b580733c7a92e61fc32b0e2303ba1f4ec3fc
3
+ metadata.gz: d683c61551360b47b9c1bdc99da1fa606f37be204668adc148d8b7a65d8ba97e
4
+ data.tar.gz: 62fc0ef126478624dd45a1bdd7b4aedb11f784cdbe6ea2700f0af0476e7cb60b
5
5
  SHA512:
6
- metadata.gz: ba70b140ad251b828e18e1746e28bafedb8a40468b0fe49a0e80f40b1e7a27296880fabca5d6888d82928d801ed9788f7aa1846e9c3a61fcfbe975cb0e460ac4
7
- data.tar.gz: '038e9c869bbff980f5eb563f412bf3c321ce61e216a2e269bf53e1e22eb58dd8180d2552b4dc1a3b9594cf3a0f691523dd443efb3c24d6c63920b3d7deba5afe'
6
+ metadata.gz: 4ef346c826fbfa6f303c723f4d63bcab1350e9de61e6b5a0bde75862194c25b1c747c81b9920c6831629a44147f3d84cdad76b6aaafde9b7f2129d0a5cb70e3f
7
+ data.tar.gz: 86f6bdfa99ae003eb16e0c08a44bdc19f18377afe54fc6e383629ea98ca1de8e7847ad0c4e1d4e758ad686b698053ad258b9f4d2e0b5213af0700a550f42ef22
data/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
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
+
10
28
  ## [v0.1.1] - 2026-08-20
11
29
 
12
30
  ### Added
@@ -21,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
21
39
 
22
40
  Initial release.
23
41
 
24
- [Unreleased]: https://github.com/aaronmallen/hanami-settings-stores/compare/0.1.1...HEAD
25
- [v0.1.1]: https://github.com/aaronmallen/hanami-settings-stores/compare/0.1.0...0.1.1
26
- [v0.1.0]: https://github.com/aaronmallen/hanami-settings-stores/releases/tag/0.1.0
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
@@ -1,16 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
3
4
  require "erb"
4
5
 
5
6
  module Hanami
6
7
  class Settings
7
8
  # A settings store backed by a local JSON, TOML, or YAML file.
8
9
  #
9
- # The file is read and parsed once, when the store is built. Its contents run through ERB
10
- # first, so a file can interpolate Ruby.
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.
11
13
  #
12
- # Only top level keys resolve. Keys are compared as strings, so a setting named
13
- # `:database_url` matches the `database_url` key in the file.
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.
14
16
  #
15
17
  # @example
16
18
  # # config/settings.yml
@@ -23,14 +25,31 @@ module Hanami
23
25
  # Hanami::Settings::FileStore.new("config/settings.yml")
24
26
  # )
25
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
+ #
26
33
  # @api public
27
34
  # @since 0.1.1
28
35
  class FileStore
36
+ # @api private
37
+ # @since 0.2.0
38
+ EMPTY_SETTINGS = {}.freeze
39
+ private_constant :EMPTY_SETTINGS
40
+
29
41
  # @api private
30
42
  # @since 0.1.1
31
43
  JSON_EXTENSIONS = %w[.json].freeze
32
44
  private_constant :JSON_EXTENSIONS
33
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
+
34
53
  # @api private
35
54
  # @since 0.1.1
36
55
  TOML_EXTENSIONS = %w[.toml].freeze
@@ -46,23 +65,25 @@ module Hanami
46
65
  EXTENSIONS = (JSON_EXTENSIONS + TOML_EXTENSIONS + YAML_EXTENSIONS).freeze
47
66
  private_constant :EXTENSIONS
48
67
 
49
- # @param filepath [String, Pathname] path to the settings file
50
- # @raise [Errno::ENOENT] if the file does not exist
51
- # @raise [ArgumentError] if the store cannot parse the file extension
52
- def initialize(filepath)
53
- source = ERB.new(File.read(filepath)).result
54
- @settings = case File.extname(filepath.to_s)
55
- when *JSON_EXTENSIONS
56
- require "json"
57
- JSON.parse(source)
58
- when *TOML_EXTENSIONS
59
- require "toml"
60
- TOML::Parser.new(source).parsed
61
- when *YAML_EXTENSIONS
62
- require "yaml"
63
- YAML.safe_load(source, aliases: true) || {}
64
- else raise ArgumentError, "cannot parse #{filepath}: expected one of #{EXTENSIONS.join(", ")}"
65
- end
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
66
87
  end
67
88
 
68
89
  # Fetches a setting value from the parsed file.
@@ -71,12 +92,69 @@ module Hanami
71
92
  # @param args [Array] optional default value
72
93
  # @yield [name] optional block for default value
73
94
  # @return [Object] the setting value
95
+ # @raise [ArgumentError] if the store cannot parse the file extension
74
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
75
98
  #
76
99
  # @api public
77
100
  # @since 0.1.1
78
101
  def fetch(name, *args, &block)
79
- @settings.fetch(name.to_s, *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
80
158
  end
81
159
  end
82
160
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-settings-stores
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.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-08-20 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
14
  name: toml
@@ -53,15 +53,15 @@ files:
53
53
  - lib/hanami/settings/composite_store.rb
54
54
  - lib/hanami/settings/file_store.rb
55
55
  - lib/hanami/settings/one_password_store.rb
56
- homepage: https://github.com/aaronmallen/hanami-settings-stores
56
+ homepage: https://aaronmallen.dev/aaron/hanami-settings-stores
57
57
  licenses:
58
58
  - MIT
59
59
  metadata:
60
- bug_tracker_uri: https://github.com/aaronmallen/hanami-settings-stores/issues
61
- changelog_uri: https://github.com/aaronmallen/hanami-settings-stores/blob/main/CHANGELOG.md
62
- 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
63
63
  rubygems_mfa_required: 'true'
64
- source_code_uri: https://github.com/aaronmallen/hanami-settings-stores
64
+ source_code_uri: https://aaronmallen.dev/aaron/hanami-settings-stores
65
65
  post_install_message:
66
66
  rdoc_options: []
67
67
  require_paths: