hanami-settings-stores 0.0.1 → 0.1.1
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 +15 -1
- data/lib/hanami/settings/bitwarden_store.rb +132 -0
- data/lib/hanami/settings/composite_store.rb +4 -3
- data/lib/hanami/settings/file_store.rb +83 -0
- data/lib/hanami/settings/one_password_store.rb +134 -0
- metadata +13 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce06f133c66bd28ca84dc455d158dea1037aafd6b09d5a3234a0c46f7ea4d7b3
|
|
4
|
+
data.tar.gz: 6a1353b35214301a89a3a43ccd34b580733c7a92e61fc32b0e2303ba1f4ec3fc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba70b140ad251b828e18e1746e28bafedb8a40468b0fe49a0e80f40b1e7a27296880fabca5d6888d82928d801ed9788f7aa1846e9c3a61fcfbe975cb0e460ac4
|
|
7
|
+
data.tar.gz: '038e9c869bbff980f5eb563f412bf3c321ce61e216a2e269bf53e1e22eb58dd8180d2552b4dc1a3b9594cf3a0f691523dd443efb3c24d6c63920b3d7deba5afe'
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [v0.1.1] - 2026-08-20
|
|
11
|
+
|
|
10
12
|
### Added
|
|
11
13
|
|
|
12
|
-
- `
|
|
14
|
+
- `FileStore` with json, toml, and yaml support
|
|
15
|
+
|
|
16
|
+
### Removed
|
|
17
|
+
|
|
18
|
+
- removed unnecessary `hanami` gem as a runtime dependency
|
|
19
|
+
|
|
20
|
+
## [v0.1.0] - 2026-03-17
|
|
21
|
+
|
|
22
|
+
Initial release.
|
|
23
|
+
|
|
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
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Hanami
|
|
7
|
+
class Settings
|
|
8
|
+
# A settings store backed by Bitwarden.
|
|
9
|
+
#
|
|
10
|
+
# Uses mappings defined via {BitwardenStore::Mixin#bw_setting} on your Settings class to
|
|
11
|
+
# resolve setting values from Bitwarden items. Requires the `bw` CLI to be installed and
|
|
12
|
+
# authenticated.
|
|
13
|
+
#
|
|
14
|
+
# Item fields are cached in memory after the first fetch for each item.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # config/settings.rb
|
|
18
|
+
# class Settings < Hanami::Settings
|
|
19
|
+
# bw_setting :database_url, item: "MyApp", constructor: Types::String
|
|
20
|
+
# bw_setting :api_key, item: "ExternalService", key: :secret_key, constructor: Types::String
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# # config/app.rb
|
|
24
|
+
# config.settings_store = Hanami::Settings::CompositeStore.new(
|
|
25
|
+
# Hanami::Settings::EnvStore.new,
|
|
26
|
+
# Hanami::Settings::BitwardenStore.new
|
|
27
|
+
# )
|
|
28
|
+
#
|
|
29
|
+
# @api public
|
|
30
|
+
# @since 0.1.0
|
|
31
|
+
class BitwardenStore
|
|
32
|
+
# DSL extension for {Hanami::Settings} that adds the {#bw_setting} method.
|
|
33
|
+
#
|
|
34
|
+
# Automatically included when this file is required.
|
|
35
|
+
#
|
|
36
|
+
# @api public
|
|
37
|
+
# @since 0.1.0
|
|
38
|
+
module Mixin
|
|
39
|
+
# @api private
|
|
40
|
+
module ClassMethods
|
|
41
|
+
# Returns the Bitwarden mappings for this settings class.
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash{Symbol => Hash}]
|
|
44
|
+
# @api private
|
|
45
|
+
def bw_mappings
|
|
46
|
+
@bw_mappings ||= {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Defines a setting backed by a Bitwarden item field.
|
|
50
|
+
#
|
|
51
|
+
# @param name [Symbol] the setting name
|
|
52
|
+
# @param item [String] the Bitwarden item name or ID
|
|
53
|
+
# @param key [Symbol, String] the field name (defaults to the setting name)
|
|
54
|
+
# @param options [Hash] additional options passed to `Dry::Configurable#setting`
|
|
55
|
+
#
|
|
56
|
+
# @api public
|
|
57
|
+
# @since 0.1.0
|
|
58
|
+
def bw_setting(name, item:, key: name, **options)
|
|
59
|
+
bw_mappings[name] = {item: item.to_s, key: key.to_s}
|
|
60
|
+
setting(name, **options)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @api private
|
|
65
|
+
def self.prepended(base)
|
|
66
|
+
base.extend(ClassMethods)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @api private
|
|
70
|
+
def initialize(store = EMPTY_STORE)
|
|
71
|
+
store.bw_mappings = self.class.bw_mappings if store.respond_to?(:bw_mappings=)
|
|
72
|
+
super
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def initialize
|
|
77
|
+
@bw_mappings = {}
|
|
78
|
+
@cache = {}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Sets the Bitwarden mappings from the settings class.
|
|
82
|
+
#
|
|
83
|
+
# @param mappings [Hash{Symbol => Hash}]
|
|
84
|
+
# @api private
|
|
85
|
+
attr_writer :bw_mappings
|
|
86
|
+
|
|
87
|
+
# Fetches a setting value using its Bitwarden mapping.
|
|
88
|
+
#
|
|
89
|
+
# @param name [String, Symbol] the setting name
|
|
90
|
+
# @param args [Array] optional default value
|
|
91
|
+
# @yield [name] optional block for default value
|
|
92
|
+
# @return [String] the field value
|
|
93
|
+
# @raise [KeyError] if the field is not found and no default is given
|
|
94
|
+
#
|
|
95
|
+
# @api public
|
|
96
|
+
# @since 0.1.0
|
|
97
|
+
def fetch(name, *args, &block)
|
|
98
|
+
mapping = @bw_mappings[name.to_sym]
|
|
99
|
+
|
|
100
|
+
unless mapping
|
|
101
|
+
return args.first unless args.empty?
|
|
102
|
+
return yield(name) if block
|
|
103
|
+
|
|
104
|
+
raise KeyError, "key not found: #{name.inspect}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
item_fields(mapping[:item]).fetch(mapping[:key], *args, &block)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
# @api private
|
|
113
|
+
def item_fields(item_name)
|
|
114
|
+
@cache[item_name] ||= load_item_fields(item_name)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @api private
|
|
118
|
+
def load_item_fields(item_name)
|
|
119
|
+
stdout, stderr, status = Open3.capture3("bw", "get", "item", item_name)
|
|
120
|
+
raise "Bitwarden CLI error: #{stderr.strip}" unless status.success?
|
|
121
|
+
|
|
122
|
+
item = JSON.parse(stdout)
|
|
123
|
+
item.fetch("fields", []).each_with_object({}) do |field, hash|
|
|
124
|
+
name = field["name"]
|
|
125
|
+
hash[name] = field["value"] unless name.nil? || name.empty?
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
Hanami::Settings.prepend(Hanami::Settings::BitwardenStore::Mixin)
|
|
@@ -17,6 +17,10 @@ module Hanami
|
|
|
17
17
|
# @api public
|
|
18
18
|
# @since 0.1.0
|
|
19
19
|
class CompositeStore
|
|
20
|
+
# @api private
|
|
21
|
+
NOT_SET = Object.new.freeze
|
|
22
|
+
private_constant :NOT_SET
|
|
23
|
+
|
|
20
24
|
# @param stores [Array<#fetch>] ordered list of stores to query
|
|
21
25
|
def initialize(*stores)
|
|
22
26
|
@stores = stores
|
|
@@ -43,9 +47,6 @@ module Hanami
|
|
|
43
47
|
|
|
44
48
|
raise KeyError, "key not found: #{name.inspect}"
|
|
45
49
|
end
|
|
46
|
-
|
|
47
|
-
NOT_SET = Object.new.freeze
|
|
48
|
-
private_constant :NOT_SET
|
|
49
50
|
end
|
|
50
51
|
end
|
|
51
52
|
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "erb"
|
|
4
|
+
|
|
5
|
+
module Hanami
|
|
6
|
+
class Settings
|
|
7
|
+
# A settings store backed by a local JSON, TOML, or YAML file.
|
|
8
|
+
#
|
|
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.
|
|
11
|
+
#
|
|
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
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# # config/settings.yml
|
|
17
|
+
# database_url: postgres://localhost/myapp
|
|
18
|
+
# session_secret: <%= ENV.fetch("SESSION_SECRET") %>
|
|
19
|
+
#
|
|
20
|
+
# # config/app.rb
|
|
21
|
+
# config.settings_store = Hanami::Settings::CompositeStore.new(
|
|
22
|
+
# Hanami::Settings::EnvStore.new,
|
|
23
|
+
# Hanami::Settings::FileStore.new("config/settings.yml")
|
|
24
|
+
# )
|
|
25
|
+
#
|
|
26
|
+
# @api public
|
|
27
|
+
# @since 0.1.1
|
|
28
|
+
class FileStore
|
|
29
|
+
# @api private
|
|
30
|
+
# @since 0.1.1
|
|
31
|
+
JSON_EXTENSIONS = %w[.json].freeze
|
|
32
|
+
private_constant :JSON_EXTENSIONS
|
|
33
|
+
|
|
34
|
+
# @api private
|
|
35
|
+
# @since 0.1.1
|
|
36
|
+
TOML_EXTENSIONS = %w[.toml].freeze
|
|
37
|
+
private_constant :TOML_EXTENSIONS
|
|
38
|
+
|
|
39
|
+
# @api private
|
|
40
|
+
# @since 0.1.1
|
|
41
|
+
YAML_EXTENSIONS = %w[.yaml .yml].freeze
|
|
42
|
+
private_constant :YAML_EXTENSIONS
|
|
43
|
+
|
|
44
|
+
# @api private
|
|
45
|
+
# @since 0.1.1
|
|
46
|
+
EXTENSIONS = (JSON_EXTENSIONS + TOML_EXTENSIONS + YAML_EXTENSIONS).freeze
|
|
47
|
+
private_constant :EXTENSIONS
|
|
48
|
+
|
|
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
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Fetches a setting value from the parsed file.
|
|
69
|
+
#
|
|
70
|
+
# @param name [String, Symbol] the setting name
|
|
71
|
+
# @param args [Array] optional default value
|
|
72
|
+
# @yield [name] optional block for default value
|
|
73
|
+
# @return [Object] the setting value
|
|
74
|
+
# @raise [KeyError] if the file has no such key and no default is given
|
|
75
|
+
#
|
|
76
|
+
# @api public
|
|
77
|
+
# @since 0.1.1
|
|
78
|
+
def fetch(name, *args, &block)
|
|
79
|
+
@settings.fetch(name.to_s, *args, &block)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Hanami
|
|
7
|
+
class Settings
|
|
8
|
+
# A settings store backed by 1Password.
|
|
9
|
+
#
|
|
10
|
+
# Uses mappings defined via {OnePasswordStore::Mixin#op_setting} on your Settings class to
|
|
11
|
+
# resolve setting values from 1Password items. Requires the `op` CLI to be installed and
|
|
12
|
+
# authenticated.
|
|
13
|
+
#
|
|
14
|
+
# Item fields are cached in memory after the first fetch for each item.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# # config/settings.rb
|
|
18
|
+
# class Settings < Hanami::Settings
|
|
19
|
+
# op_setting :database_url, item: "MyApp", constructor: Types::String
|
|
20
|
+
# op_setting :api_key, item: "ExternalService", key: :secret_key, constructor: Types::String
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# # config/app.rb
|
|
24
|
+
# config.settings_store = Hanami::Settings::CompositeStore.new(
|
|
25
|
+
# Hanami::Settings::EnvStore.new,
|
|
26
|
+
# Hanami::Settings::OnePasswordStore.new(vault: "Production")
|
|
27
|
+
# )
|
|
28
|
+
#
|
|
29
|
+
# @api public
|
|
30
|
+
# @since 0.1.0
|
|
31
|
+
class OnePasswordStore
|
|
32
|
+
# DSL extension for {Hanami::Settings} that adds the {#op_setting} method.
|
|
33
|
+
#
|
|
34
|
+
# Automatically included when this file is required.
|
|
35
|
+
#
|
|
36
|
+
# @api public
|
|
37
|
+
# @since 0.1.0
|
|
38
|
+
module Mixin
|
|
39
|
+
# @api private
|
|
40
|
+
module ClassMethods
|
|
41
|
+
# Returns the 1Password mappings for this settings class.
|
|
42
|
+
#
|
|
43
|
+
# @return [Hash{Symbol => Hash}]
|
|
44
|
+
# @api private
|
|
45
|
+
def op_mappings
|
|
46
|
+
@op_mappings ||= {}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Defines a setting backed by a 1Password item field.
|
|
50
|
+
#
|
|
51
|
+
# @param name [Symbol] the setting name
|
|
52
|
+
# @param item [String] the 1Password item name or ID
|
|
53
|
+
# @param key [Symbol, String] the field label (defaults to the setting name)
|
|
54
|
+
# @param options [Hash] additional options passed to `Dry::Configurable#setting`
|
|
55
|
+
#
|
|
56
|
+
# @api public
|
|
57
|
+
# @since 0.1.0
|
|
58
|
+
def op_setting(name, item:, key: name, **options)
|
|
59
|
+
op_mappings[name] = {item: item.to_s, key: key.to_s}
|
|
60
|
+
setting(name, **options)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @api private
|
|
65
|
+
def self.prepended(base)
|
|
66
|
+
base.extend(ClassMethods)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @api private
|
|
70
|
+
def initialize(store = EMPTY_STORE)
|
|
71
|
+
store.op_mappings = self.class.op_mappings if store.respond_to?(:op_mappings=)
|
|
72
|
+
super
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @param vault [String] the 1Password vault name or ID
|
|
77
|
+
def initialize(vault:)
|
|
78
|
+
@vault = vault
|
|
79
|
+
@op_mappings = {}
|
|
80
|
+
@cache = {}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Sets the 1Password mappings from the settings class.
|
|
84
|
+
#
|
|
85
|
+
# @param mappings [Hash{Symbol => Hash}]
|
|
86
|
+
# @api private
|
|
87
|
+
attr_writer :op_mappings
|
|
88
|
+
|
|
89
|
+
# Fetches a setting value using its 1Password mapping.
|
|
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 [String] the field value
|
|
95
|
+
# @raise [KeyError] if the field is not found and no default is given
|
|
96
|
+
#
|
|
97
|
+
# @api public
|
|
98
|
+
# @since 0.1.0
|
|
99
|
+
def fetch(name, *args, &block)
|
|
100
|
+
mapping = @op_mappings[name.to_sym]
|
|
101
|
+
|
|
102
|
+
unless mapping
|
|
103
|
+
return args.first unless args.empty?
|
|
104
|
+
return yield(name) if block
|
|
105
|
+
|
|
106
|
+
raise KeyError, "key not found: #{name.inspect}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
item_fields(mapping[:item]).fetch(mapping[:key], *args, &block)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# @api private
|
|
115
|
+
def item_fields(item_name)
|
|
116
|
+
@cache[item_name] ||= load_item_fields(item_name)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @api private
|
|
120
|
+
def load_item_fields(item_name)
|
|
121
|
+
stdout, stderr, status = Open3.capture3("op", "item", "get", item_name, "--vault", @vault, "--format", "json")
|
|
122
|
+
raise "1Password CLI error: #{stderr.strip}" unless status.success?
|
|
123
|
+
|
|
124
|
+
item = JSON.parse(stdout)
|
|
125
|
+
item.fetch("fields", []).each_with_object({}) do |field, hash|
|
|
126
|
+
label = field["label"]
|
|
127
|
+
hash[label] = field["value"] unless label.nil? || label.empty?
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
Hanami::Settings.prepend(Hanami::Settings::OnePasswordStore::Mixin)
|
metadata
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hanami-settings-stores
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.1.1
|
|
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-08-20 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
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '2
|
|
33
|
+
version: '2'
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '2
|
|
41
|
-
description:
|
|
42
|
-
|
|
40
|
+
version: '2'
|
|
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: []
|
|
@@ -49,7 +49,10 @@ files:
|
|
|
49
49
|
- CHANGELOG.md
|
|
50
50
|
- LICENSE
|
|
51
51
|
- lib/hanami-settings-stores.rb
|
|
52
|
+
- lib/hanami/settings/bitwarden_store.rb
|
|
52
53
|
- lib/hanami/settings/composite_store.rb
|
|
54
|
+
- lib/hanami/settings/file_store.rb
|
|
55
|
+
- lib/hanami/settings/one_password_store.rb
|
|
53
56
|
homepage: https://github.com/aaronmallen/hanami-settings-stores
|
|
54
57
|
licenses:
|
|
55
58
|
- MIT
|
|
@@ -77,5 +80,5 @@ requirements: []
|
|
|
77
80
|
rubygems_version: 3.4.19
|
|
78
81
|
signing_key:
|
|
79
82
|
specification_version: 4
|
|
80
|
-
summary:
|
|
83
|
+
summary: Pluggable settings stores for Hanami
|
|
81
84
|
test_files: []
|