strato_env 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f79b3af1cf5e9b6351c48ecc18b5bce119962fc5a936c90fe74d11b61afaaaa5
4
+ data.tar.gz: 74da0decc7537880ef50dd89085a17ca59c9300385010951daea8879f0cbd241
5
+ SHA512:
6
+ metadata.gz: b88ad19cc8e7e566d547949b1bf814604f68d4e2c23984cde6802e70b8146989e08ef3f7bdff6dff3cd9065c404df5bea3f448ac1c87f68fb65caf7139148ddf
7
+ data.tar.gz: fdd5fd297ec109cb077e734a7e827da4d063c25ee9da91c24aac9571a7322d064ea9b39b8cdad6fe0c7a276a1229b0c68f613737e6b4001cec4d9e1bb787080c
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-05-15
4
+
5
+ ## Added
6
+
7
+ - Initial release. Load one or more AWS SSM Parameter Store paths into ENV at boot, with later paths overriding earlier ones.
8
+ - Pluggable fetcher contract: anything that responds to `#call(path)` and returns `Hash<String, String>` works as a backend. Default is `StratoEnv::SSMFetcher`.
9
+ - `StratoEnv.fetch(paths:)` returns the merged hash without touching ENV — for previewing or applying elsewhere.
10
+ - `StratoEnv.apply(hash)` writes a hash to ENV — for composing `fetch` with custom merge/diff logic before applying.
11
+
12
+ [0.1.0]: https://github.com/samsonjs/strato_env/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Sami Samhuri
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,194 @@
1
+ # strato_env
2
+
3
+ A small Ruby gem that loads layered configuration into `ENV` at boot. Point it at one or more paths and it sets `ENV` vars from the values fetched. Multiple paths form layers, with later paths overriding earlier ones — so you can split common config from host-specific or environment-specific overrides.
4
+
5
+ The default backend is [AWS SSM Parameter Store][ssm], but the loader is decoupled from the backend: pass any callable that takes a path and returns a `Hash<String, String>` (anything responding to `#call(path)` — `Proc`, lambda, `Method`, or a class with `#call` defined). That makes testing trivial and adding new backends (Secrets Manager, Vault, a YAML file) a few lines of glue.
6
+
7
+ No Rails dependency. Works in Rails, Sinatra, Lambda, scripts, or any Ruby boot process.
8
+
9
+ The name comes from the Italian word *strato*, meaning "layer" — the gem is about layering configuration.
10
+
11
+ [ssm]: https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html
12
+
13
+ ---
14
+
15
+ - [Quick start](#quick-start)
16
+ - [Layered loading](#layered-loading)
17
+ - [Preview without applying](#preview-without-applying)
18
+ - [Custom orchestration: fetch + apply](#custom-orchestration-fetch--apply)
19
+ - [Where to call it from](#where-to-call-it-from)
20
+ - [Customising the SSM fetcher](#customising-the-ssm-fetcher)
21
+ - [Custom fetchers](#custom-fetchers)
22
+ - [Testing](#testing)
23
+ - [License](#license)
24
+ - [Contribution guide](#contribution-guide)
25
+
26
+ ## Quick start
27
+
28
+ ```
29
+ $ gem install strato_env
30
+ ```
31
+
32
+ ```ruby
33
+ require "strato_env"
34
+
35
+ StratoEnv.load(paths: "/myapp/staging/")
36
+ # ENV is now populated with one entry per parameter under that path.
37
+ ```
38
+
39
+ A parameter named `/myapp/staging/DATABASE_URL` becomes `ENV["DATABASE_URL"]`. By default, `SecureString` parameters are decrypted automatically.
40
+
41
+ `.load` returns an `Array<String>` of the ENV var names that were set.
42
+
43
+ ## Layered loading
44
+
45
+ Pass an array to layer multiple paths. Later paths override earlier ones, so the rightmost path wins on collisions.
46
+
47
+ ```ruby
48
+ rails_env = ENV.fetch("RAILS_ENV") # "staging"
49
+ role = ENV.fetch("HOST_ROLE") # "web" or "worker"
50
+ extra_layers = ENV.fetch("EXTRA_LAYERS", "") # "ruby4_0,blah,..."
51
+
52
+ StratoEnv.load(paths: [
53
+ "/myapp/#{rails_env}/common/",
54
+ "/myapp/#{rails_env}/#{role}/",
55
+ *extra_layers.split(',').map { "/myapp/#{rails_env}/#{it}/" },
56
+ ])
57
+ ```
58
+
59
+ This pattern lets you keep shared config (database URL, API keys) under `common/` and override only the host-specific values (queue URLs, log destinations) under `web/` or `worker/`.
60
+
61
+ I also use this for validating a new deployment of an environment before promoting it, e.g. for upgrading from Ruby 3.4 to 4.0. Sometimes you have to override a couple things so I'll layer on those refinements with `/myapp/staging/ruby4_0/`.
62
+
63
+ ## Preview without applying
64
+
65
+ `StratoEnv.fetch` returns the merged hash without touching ENV. Useful for previewing, logging, or applying to somewhere other than `ENV`.
66
+
67
+ ```ruby
68
+ StratoEnv.fetch(paths: ["/myapp/common/", "/myapp/web/"])
69
+ # => { "DATABASE_URL" => "postgres://...", "LOG_LEVEL" => "debug" }
70
+ ```
71
+
72
+ ## Custom orchestration: fetch + apply
73
+
74
+ For more complex flows — e.g. merging from multiple namespaces, diffing them, warning on overrides — fetch each source separately, merge however you like, then `apply` the result. `StratoEnv.load(paths:)` is just sugar for `apply(fetch(paths: paths))`.
75
+
76
+ ```ruby
77
+ loader = StratoEnv.new
78
+
79
+ # Fetch two namespaces independently (no ENV side effects yet).
80
+ terraform_env = loader.fetch(paths: ["/terraform/myapp/common/", "/terraform/myapp/web/"])
81
+ human_env = loader.fetch(paths: ["/myapp/common/", "/myapp/web/"])
82
+
83
+ # Apply your own policy. Here: humans win, but warn on divergence.
84
+ terraform_env.each do |name, tf_value|
85
+ next unless human_env.key?(name) && human_env[name] != tf_value
86
+ warn("[env] #{name}: human value overrides terraform.")
87
+ end
88
+
89
+ loader.apply(terraform_env.merge(human_env))
90
+ ```
91
+
92
+ `#apply(hash)` writes the hash to ENV and returns the array of names set, same return shape as `#load`. Also available as the class method `StratoEnv.apply(hash)`.
93
+
94
+ ## Where to call it from
95
+
96
+ Call `StratoEnv.load` as early in boot as possible, before any code reads the ENV vars it needs to populate.
97
+
98
+ ### Rails
99
+
100
+ For Rails apps, put the call at the top of your environment file, before `Rails.application.configure`. The configure block typically reads ENV (`ENV.fetch("REDIS_URL")`, etc.), so the fetcher has to run first.
101
+
102
+ ```ruby
103
+ # config/environments/production.rb
104
+ require "strato_env"
105
+
106
+ rails_env = ENV.fetch("RAILS_ENV")
107
+ role = ENV.fetch("HOST_ROLE")
108
+
109
+ StratoEnv.load(paths: [
110
+ "/myapp/#{rails_env}/common/",
111
+ "/myapp/#{rails_env}/#{role}/",
112
+ ])
113
+
114
+ Rails.application.configure do
115
+ # ... now safe to read ENV ...
116
+ end
117
+ ```
118
+
119
+ `config/initializers/*.rb` runs too late — the environment file's body has already executed by then.
120
+
121
+ ### Lambda / scripts
122
+
123
+ Just call it before reading the values:
124
+
125
+ ```ruby
126
+ require "strato_env"
127
+
128
+ StratoEnv.load(paths: "/myapp/prod/")
129
+ # ... rest of your script ...
130
+ ```
131
+
132
+ ## Customising the SSM fetcher
133
+
134
+ The default fetcher is `StratoEnv::SSMFetcher`. To pass options to it (recursive lookups, decryption off, a pre-configured client), build it yourself and pass it to `StratoEnv.new`:
135
+
136
+ ```ruby
137
+ fetcher = StratoEnv::SSMFetcher.new(
138
+ recursive: true, # fetch parameters nested under the path
139
+ with_decryption: false, # leave SecureString values encrypted
140
+ client: my_ssm_client, # custom region/credentials
141
+ )
142
+
143
+ StratoEnv.new(fetcher: fetcher).load(paths: "/myapp/staging/")
144
+ ```
145
+
146
+ `StratoEnv.load` and `StratoEnv.fetch` are class-method shortcuts for `StratoEnv.new.load` / `StratoEnv.new.fetch` — i.e. the default `SSMFetcher` with all defaults.
147
+
148
+ ## Custom fetchers
149
+
150
+ A fetcher is any callable that takes a path and returns a `Hash<String, String>`. Anything that responds to `#call(path)` works: a `Proc`, a lambda, a `Method` object, or a class with `#call` defined.
151
+
152
+ ```ruby
153
+ yaml_fetcher = ->(path) { YAML.load_file(path) }
154
+ StratoEnv.new(fetcher: yaml_fetcher).load(paths: "config/app.yml")
155
+ ```
156
+
157
+ This is what makes the loader trivial to test: pass a stub fetcher in your specs and assert the right keys land in `ENV`. No AWS SDK mocking required.
158
+
159
+ ```ruby
160
+ StubFetcher = Data.define(:values) do
161
+ def call(_path) = values
162
+ end
163
+
164
+ stub = StubFetcher.new(values: { "DATABASE_URL" => "postgres://test" })
165
+ StratoEnv.new(fetcher: stub).load(paths: "/anything/")
166
+
167
+ ENV["DATABASE_URL"] # => "postgres://test"
168
+ ```
169
+
170
+ ## Testing
171
+
172
+ For testing the bundled `SSMFetcher`, the `aws-sdk-ssm` gem ships with stub support:
173
+
174
+ ```ruby
175
+ client = Aws::SSM::Client.new(stub_responses: true, region: "us-east-1")
176
+ client.stub_responses(:get_parameters_by_path, {
177
+ parameters: [
178
+ { name: "/myapp/test/DATABASE_URL", value: "postgres://localhost/test", type: "String" },
179
+ ],
180
+ })
181
+
182
+ StratoEnv::SSMFetcher.new(client: client).call("/myapp/test/")
183
+ # => { "DATABASE_URL" => "postgres://localhost/test" }
184
+ ```
185
+
186
+ For testing your *loader* logic, prefer a stub fetcher (see [Custom fetchers](#custom-fetchers)) — no AWS SDK setup needed.
187
+
188
+ ## License
189
+
190
+ The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
191
+
192
+ ## Contribution guide
193
+
194
+ Pull requests are welcome! Make sure that new code is reasonably well tested and all the checks pass. I'm happy to provide a bit of direction and guidance if you're unsure how to proceed with any of these things.
@@ -0,0 +1,40 @@
1
+ require "aws-sdk-ssm"
2
+
3
+ class StratoEnv
4
+ # SSM Parameter Store implementation of the fetcher protocol used by
5
+ # {StratoEnv}.
6
+ #
7
+ # Given a path, returns a +Hash<String, String>+ mapping each parameter's
8
+ # basename (last path component) to its value. Pages are followed
9
+ # transparently and +SecureString+ values are decrypted by default.
10
+ #
11
+ # @example
12
+ # fetcher = StratoEnv::SSMFetcher.new(recursive: true)
13
+ # fetcher.call("/myapp/")
14
+ # # => { "DATABASE_URL" => "postgres://...", "REDIS_URL" => "redis://..." }
15
+ class SSMFetcher
16
+ # @param client [Aws::SSM::Client, nil] an SSM client. Defaults to a new
17
+ # {Aws::SSM::Client} configured from the ambient AWS environment.
18
+ # @param recursive [Boolean] whether to fetch parameters nested beneath the
19
+ # given path. Defaults to +false+ to match the common one-level layout.
20
+ # @param with_decryption [Boolean] decrypt +SecureString+ parameters in
21
+ # flight. Defaults to +true+.
22
+ def initialize(client: nil, recursive: false, with_decryption: true)
23
+ @client = client || Aws::SSM::Client.new
24
+ @recursive = recursive
25
+ @with_decryption = with_decryption
26
+ end
27
+
28
+ # Fetch all parameters under a single SSM path.
29
+ #
30
+ # @param path [String] the SSM path to query (typically with a trailing slash).
31
+ # @return [Hash<String, String>] parameter basenames mapped to their values.
32
+ def call(path)
33
+ @client.get_parameters_by_path(
34
+ path: path, recursive: @recursive, with_decryption: @with_decryption
35
+ ).each_page.flat_map(&:parameters).to_h do |param|
36
+ [param.name.split("/").last, param.value]
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ class StratoEnv
2
+ VERSION = "0.1.0".freeze
3
+ end
data/lib/strato_env.rb ADDED
@@ -0,0 +1,85 @@
1
+ require_relative "strato_env/version"
2
+ require_relative "strato_env/ssm_fetcher"
3
+
4
+ # StratoEnv loads layered configuration into ENV at boot.
5
+ #
6
+ # A loader composes one or more *paths* worth of key/value pairs and writes
7
+ # them to +ENV+. The actual fetching of values is delegated to a *fetcher*,
8
+ # any callable that takes a path and returns a +Hash<String, String>+. The
9
+ # default fetcher is {SSMFetcher}, which reads from AWS SSM Parameter Store,
10
+ # but you can inject any callable (a +Proc+, a lambda, a +Method+, or a class
11
+ # with +#call+ defined). This makes it easy to:
12
+ #
13
+ # - Test loader logic without touching AWS by passing a stub fetcher.
14
+ # - Add other backends (Secrets Manager, Vault, a YAML file) without changing
15
+ # the loader.
16
+ # - Preview values before applying them via {#fetch}.
17
+ #
18
+ # When multiple paths are passed, they form layers: later paths override
19
+ # earlier ones (right-most wins on collision).
20
+ #
21
+ # @example Default usage (SSM)
22
+ # StratoEnv.load(paths: ["/myapp/common/", "/myapp/web/"])
23
+ #
24
+ # @example Inspect values without touching ENV
25
+ # StratoEnv.fetch(paths: "/myapp/web/")
26
+ # # => { "DATABASE_URL" => "postgres://...", "REDIS_URL" => "redis://..." }
27
+ #
28
+ # @example Customise the SSM fetcher
29
+ # fetcher = StratoEnv::SSMFetcher.new(recursive: true)
30
+ # StratoEnv.new(fetcher: fetcher).load(paths: "/myapp/")
31
+ #
32
+ # @example Inject any callable as a fetcher
33
+ # yaml_fetcher = ->(path) { YAML.load_file(path) }
34
+ # StratoEnv.new(fetcher: yaml_fetcher).load(paths: "config/app.yml")
35
+ class StratoEnv
36
+ # @param fetcher [#call] any callable that takes a path and returns a
37
+ # +Hash<String, String>+. Anything that responds to +#call(path)+ works:
38
+ # a +Proc+, a lambda, a +Method+ object, or a class with +#call+ defined.
39
+ # Defaults to a new {SSMFetcher}.
40
+ def initialize(fetcher: SSMFetcher.new)
41
+ @fetcher = fetcher
42
+ end
43
+
44
+ # Fetch values for the given paths and write them to ENV.
45
+ #
46
+ # Equivalent to +apply(fetch(paths: paths))+.
47
+ #
48
+ # @param paths [String, Array<String>] one or more paths to load, in order.
49
+ # Later paths override earlier ones on key collision.
50
+ # @return [Array<String>] the names of the ENV vars that were set.
51
+ def load(paths:)
52
+ apply(fetch(paths: paths))
53
+ end
54
+
55
+ # Fetch values for the given paths and return them as a merged hash without
56
+ # touching ENV. Useful for previewing, testing, or applying the values to
57
+ # somewhere other than ENV.
58
+ #
59
+ # @param paths [String, Array<String>] one or more paths to load, in order.
60
+ # Later paths override earlier ones on key collision.
61
+ # @return [Hash<String, String>] merged key/value pairs across all paths.
62
+ def fetch(paths:)
63
+ Array(paths).map { |path| @fetcher.call(path) }.reduce({}, :merge)
64
+ end
65
+
66
+ # Write the given hash to ENV. Useful for composing with {#fetch} when you
67
+ # want custom logic between fetching and applying (e.g. diffing across
68
+ # namespaces and warning on overrides).
69
+ #
70
+ # @param values [Hash<String, String>] keys become ENV var names.
71
+ # @return [Array<String>] the names that were set in ENV.
72
+ def apply(values) = self.class.apply(values)
73
+
74
+ # @see #load
75
+ def self.load(paths:) = new.load(paths: paths)
76
+
77
+ # @see #fetch
78
+ def self.fetch(paths:) = new.fetch(paths: paths)
79
+
80
+ # @see #apply
81
+ def self.apply(values)
82
+ values.each { |name, value| ENV[name] = value }
83
+ values.keys
84
+ end
85
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: strato_env
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sami Samhuri
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: aws-sdk-ssm
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1'
26
+ description: |
27
+ StratoEnv populates ENV from one or more AWS SSM Parameter Store paths.
28
+ Multiple paths form layers, with later paths overriding earlier ones, so
29
+ you can split common config from node-specific or environment-specific
30
+ overrides. No Rails dependency; works in Rails, Sinatra, Lambda, scripts,
31
+ or any Ruby boot process.
32
+ email:
33
+ - sami@samhuri.net
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - CHANGELOG.md
39
+ - LICENSE.txt
40
+ - README.md
41
+ - lib/strato_env.rb
42
+ - lib/strato_env/ssm_fetcher.rb
43
+ - lib/strato_env/version.rb
44
+ homepage: https://github.com/samsonjs/strato_env
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ bug_tracker_uri: https://github.com/samsonjs/strato_env/issues
49
+ changelog_uri: https://github.com/samsonjs/strato_env/releases
50
+ source_code_uri: https://github.com/samsonjs/strato_env
51
+ homepage_uri: https://github.com/samsonjs/strato_env
52
+ rubygems_mfa_required: 'true'
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 3.2.0
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubygems_version: 4.0.11
68
+ specification_version: 4
69
+ summary: Load layered AWS SSM Parameter Store values into ENV at boot
70
+ test_files: []