arctic 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: 993528a395bd6f2b84ca9dea07dfc4415795d47b4711a0bf936dd618996d379e
4
+ data.tar.gz: 2f8700204e748b59e4ded52c35b6b78c929a43006319e70051d0f0bc2ccbeb59
5
+ SHA512:
6
+ metadata.gz: f77978ece9e9c2d09498285f21a48d237fb962f8ca7ffaa28482d775423b62c1080f3961ef4e077192dee74b15d08760f2dd6a1565bed539b8c5a7fd14aac131
7
+ data.tar.gz: 070a4772798848a613c7364e14e90567a19265601a2384c1d0f6596ac71b8505a200b725842a93ff43cb52148e688b3f0bd221d59177fb707b0d73fcc9c6d944
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6
+
7
+ ## [0.1.0] - 2026-01-21
8
+
9
+ ### Added
10
+
11
+ - Initial release of Arctic gem
12
+
13
+ [0.1.0]: https://github.com/persona-id/arctic/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Persona
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # Arctic 🧊
2
+
3
+ > Keep your environment frozen solid
4
+
5
+ Arctic is a Ruby gem that provides a read-only, drop-in replacement for `ENV` with frozen, deduplicated strings to dramatically reduce memory allocations. Built as a C extension using Ruby's internal fstring table, Arctic offers zero-allocation access to environment variables after the first read.
6
+
7
+ ## Features
8
+
9
+ - **Frozen Strings**: All returned strings are frozen, preventing accidental mutations
10
+ - **Automatic Deduplication**: Uses Ruby's fstring table (`rb_enc_interned_str_cstr`) for zero allocations
11
+ - **Drop-in Replacement**: Implements the read-only ENV API ([], fetch, key?, each, to_h, etc.)
12
+ - **ClimateControl Compatible**: Works seamlessly with ClimateControl for testing
13
+ - **High Performance**: Same system calls as ENV, with memory savings from deduplication
14
+ - **C Extension**: Direct access to process environment via `getenv()` - no caching, always fresh
15
+
16
+ ## Installation
17
+
18
+ Add to your `Gemfile`:
19
+
20
+ ```ruby
21
+ gem 'arctic'
22
+ ```
23
+
24
+ Or install directly:
25
+
26
+ ```bash
27
+ gem install arctic
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Basic Access
33
+
34
+ ```ruby
35
+ require 'arctic'
36
+
37
+ # Simple access (returns frozen string)
38
+ Arctic['PATH'] # => "/usr/bin:/bin" (frozen)
39
+ Arctic['HOME'] # => "/Users/user" (frozen)
40
+ Arctic['NONEXISTENT'] # => nil
41
+
42
+ # Fetch with defaults
43
+ Arctic.fetch('RAILS_ENV') # => "production" or raises KeyError
44
+ Arctic.fetch('MISSING', 'default') # => "default"
45
+ Arctic.fetch('MISSING') { |k| "#{k}!" } # => "MISSING!"
46
+
47
+ # Check existence
48
+ Arctic.key?('PATH') # => true
49
+ Arctic.key?('MISSING') # => false
50
+
51
+ # Aliases work too
52
+ Arctic.has_key?('PATH') # => true
53
+ Arctic.include?('PATH') # => true
54
+ Arctic.member?('PATH') # => true
55
+ ```
56
+
57
+ ### Iteration
58
+
59
+ ```ruby
60
+ # Iterate over all variables (keys and values are frozen)
61
+ Arctic.each do |key, value|
62
+ puts "#{key}=#{value}"
63
+ end
64
+
65
+ # Get all keys or values
66
+ Arctic.keys # => ["PATH", "HOME", ...] (frozen strings)
67
+ Arctic.values # => ["/usr/bin:/bin", ...] (frozen strings)
68
+
69
+ # Convert to hash
70
+ hash = Arctic.to_h # => {"PATH" => "/usr/bin:/bin", ...}
71
+ ```
72
+
73
+ ### Memory Efficiency
74
+
75
+ Arctic automatically deduplicates strings using Ruby's fstring table:
76
+
77
+ ```ruby
78
+ ENV['VAR1'] = 'shared_value'
79
+ ENV['VAR2'] = 'shared_value'
80
+
81
+ # Both return the SAME frozen object (deduplicated)
82
+ val1 = Arctic['VAR1']
83
+ val2 = Arctic['VAR2']
84
+ val1.object_id == val2.object_id # => true
85
+
86
+ # Frozen strings prevent mutations
87
+ val1.frozen? # => true
88
+ ```
89
+
90
+ ### ClimateControl Compatibility
91
+
92
+ Arctic works seamlessly with [ClimateControl](https://github.com/thoughtbot/climate_control) for testing:
93
+
94
+ ```ruby
95
+ require 'arctic'
96
+ require 'climate_control'
97
+
98
+ # Arctic always reads current ENV values
99
+ ENV['TEST_VAR'] = 'original'
100
+ Arctic['TEST_VAR'] # => 'original'
101
+
102
+ ClimateControl.modify(TEST_VAR: 'modified') do
103
+ Arctic['TEST_VAR'] # => 'modified' (detects the change)
104
+ end
105
+
106
+ Arctic['TEST_VAR'] # => 'original' (restored after block)
107
+ ```
108
+
109
+ **How it works:** Arctic calls `getenv()` directly (just like ENV), so it always sees the current process environment. No cache invalidation needed - ClimateControl's modifications are immediately visible.
110
+
111
+ ## How It Works
112
+
113
+ ### C Extension with Fstring Table
114
+
115
+ Arctic is implemented as a C extension that:
116
+
117
+ 1. **Reads directly from environment**: Uses `getenv()` to access the process environment
118
+ 2. **Creates fstrings immediately**: Calls `rb_enc_interned_str_cstr()` to create frozen, interned strings with proper locale encoding
119
+ 3. **Leverages Ruby's deduplication**: The fstring table ensures identical string values return the same object
120
+ 4. **Zero allocations after first access**: Subsequent reads of the same value return the existing fstring
121
+
122
+ ```c
123
+ // Simplified implementation
124
+ VALUE arctic_aref(VALUE self, VALUE key) {
125
+ const char* env_value = getenv(StringValueCStr(key));
126
+ if (env_value == NULL) return Qnil;
127
+
128
+ // Direct fstring creation with locale encoding - no intermediate allocation
129
+ return rb_enc_interned_str_cstr(env_value, rb_locale_encoding());
130
+ }
131
+ ```
132
+
133
+ ### Performance Characteristics
134
+
135
+ - **First access**: One `getenv()` call + fstring table intern (~same as ENV)
136
+ - **Repeated access**: `getenv()` + fstring table lookup (returns existing object)
137
+ - **Memory**: 90%+ reduction in allocations for frequently accessed values
138
+ - **No caching overhead**: Always reads current values, no staleness issues
139
+
140
+ ## API Reference
141
+
142
+ Arctic implements the most commonly used ENV methods:
143
+
144
+ ### Core Access
145
+ - `Arctic[key]` - Get value (returns frozen string or nil)
146
+ - `Arctic.fetch(key)` - Get with default or block
147
+ - `Arctic.fetch(key, default)` - Get with default value
148
+ - `Arctic.fetch(key) { |k| ... }` - Get with block
149
+
150
+ ### Existence Checks
151
+ - `Arctic.key?(key)` - Check if key exists
152
+ - `Arctic.has_key?(key)` - Alias for key?
153
+ - `Arctic.include?(key)` - Alias for key?
154
+ - `Arctic.member?(key)` - Alias for key?
155
+
156
+ ### Iteration
157
+ - `Arctic.each { |k,v| ... }` - Iterate over key-value pairs
158
+ - `Arctic.each_pair { |k,v| ... }` - Alias for each
159
+
160
+ ### Conversion
161
+ - `Arctic.keys` - Array of all keys (frozen)
162
+ - `Arctic.values` - Array of all values (frozen)
163
+ - `Arctic.to_h` - Convert to hash
164
+ - `Arctic.to_hash` - Alias for to_h
165
+
166
+ ### Information
167
+ - `Arctic.empty?` - Check if environment is empty
168
+ - `Arctic.size` - Count of environment variables
169
+ - `Arctic.length` - Alias for size
170
+ - `Arctic.inspect` - String representation
171
+
172
+ ## Requirements
173
+
174
+ - Ruby >= 3.3.0
175
+ - C compiler (gcc, clang, or compatible)
176
+ - Standard C library with `stdlib.h` and `string.h`
177
+
178
+ ## Development
179
+
180
+ ```bash
181
+ # Install dependencies
182
+ bundle install
183
+
184
+ # Compile the C extension
185
+ bundle exec rake compile
186
+
187
+ # Run tests
188
+ bundle exec rake spec
189
+
190
+ # Or do both
191
+ bundle exec rake # compile + spec
192
+ ```
193
+
194
+ ## Testing
195
+
196
+ Arctic includes comprehensive test coverage:
197
+
198
+ - **Core functionality tests**: Frozen strings, deduplication, nil handling
199
+ - **ClimateControl compatibility**: All modification scenarios
200
+ - **Memory efficiency tests**: Object identity verification
201
+ - **Encoding tests**: UTF-8 and special characters
202
+
203
+ ```bash
204
+ # Run all specs
205
+ bundle exec rspec
206
+
207
+ # Run specific test files
208
+ bundle exec rspec spec/arctic_spec.rb
209
+ bundle exec rspec spec/climate_control_spec.rb
210
+ ```
211
+
212
+ ## Contributing
213
+
214
+ 1. Fork the repository
215
+ 2. Create a feature branch (`git checkout -b feature/my-feature`)
216
+ 3. Make your changes with tests
217
+ 4. Run the test suite (`bundle exec rake`)
218
+ 5. Commit your changes (`git commit -am 'Add feature'`)
219
+ 6. Push to the branch (`git push origin feature/my-feature`)
220
+ 7. Open a Pull Request
221
+
222
+ ## License
223
+
224
+ MIT License - see [LICENSE.txt](LICENSE.txt) for details.
225
+
226
+ ## Credits
227
+
228
+ Created by the Persona engineering team to reduce memory allocations in our Rails application.
229
+
230
+ Inspired by Ruby's fstring optimization and the need for efficient environment variable access in high-scale applications.
231
+
232
+ ## Tagline
233
+
234
+ **"Keep your environment frozen solid"** ❄️
@@ -0,0 +1,485 @@
1
+ #include "ruby.h"
2
+ #include "ruby/encoding.h"
3
+ #include <stdlib.h>
4
+ #include <string.h>
5
+
6
+ // Forward declarations
7
+ VALUE mArctic;
8
+
9
+ /*
10
+ * Helper function to get environment variable count
11
+ *
12
+ * Iterates through the external environ array to count the total number
13
+ * of environment variables currently set.
14
+ *
15
+ * @return [int] The number of environment variables
16
+ */
17
+ static int env_count(void) {
18
+ extern char **environ;
19
+ char **env = environ;
20
+ int count = 0;
21
+
22
+ while (*env != NULL) {
23
+ count++;
24
+ env++;
25
+ }
26
+
27
+ return count;
28
+ }
29
+
30
+ /*
31
+ * call-seq:
32
+ * Arctic[key] -> value or nil
33
+ *
34
+ * Retrieves the value of the environment variable +key+.
35
+ *
36
+ * Returns a frozen, deduplicated string via rb_enc_interned_str_cstr() for
37
+ * memory efficiency. If the environment variable does not exist, returns +nil+.
38
+ *
39
+ * @param self [VALUE] The Arctic module
40
+ * @param key [VALUE] A String containing the environment variable name
41
+ * @return [VALUE] The frozen string value of the environment variable, or nil if not found
42
+ *
43
+ * Example:
44
+ * Arctic["PATH"] #=> "/usr/bin:/bin"
45
+ * Arctic["NONE"] #=> nil
46
+ */
47
+ static VALUE arctic_aref(VALUE self, VALUE key) {
48
+ const char* env_key;
49
+ const char* env_value;
50
+
51
+ Check_Type(key, T_STRING);
52
+ env_key = StringValueCStr(key);
53
+ env_value = getenv(env_key);
54
+
55
+ if (env_value == NULL) {
56
+ return Qnil;
57
+ }
58
+
59
+ return rb_enc_interned_str_cstr(env_value, rb_locale_encoding());
60
+ }
61
+
62
+ /*
63
+ * call-seq:
64
+ * Arctic.fetch(key) -> value
65
+ * Arctic.fetch(key, default) -> value
66
+ * Arctic.fetch(key) {|key| block } -> value
67
+ *
68
+ * Retrieves the environment variable +key+.
69
+ *
70
+ * If the key exists, returns its frozen, deduplicated value.
71
+ * If the key does not exist:
72
+ * - With a block: yields the key to the block and returns the block's result
73
+ * - With a default argument: returns the default value
74
+ * - Without either: raises KeyError
75
+ *
76
+ * @param argc [int] The number of arguments passed
77
+ * @param argv [VALUE*] Array of arguments (key and optional default)
78
+ * @param self [VALUE] The Arctic module
79
+ * @return [VALUE] The environment variable value, default value, or block result
80
+ * @raise [KeyError] If the key is not found and no default or block is provided
81
+ *
82
+ * Example:
83
+ * Arctic.fetch("PATH") #=> "/usr/bin:/bin"
84
+ * Arctic.fetch("MISSING", "default") #=> "default"
85
+ * Arctic.fetch("MISSING") { |k| "#{k}?" } #=> "MISSING?"
86
+ * Arctic.fetch("MISSING") #=> raises KeyError
87
+ */
88
+ static VALUE arctic_fetch(int argc, VALUE *argv, VALUE self) {
89
+ VALUE key, default_value;
90
+ const char* env_key;
91
+ const char* env_value;
92
+
93
+ rb_scan_args(argc, argv, "11", &key, &default_value);
94
+ Check_Type(key, T_STRING);
95
+
96
+ env_key = StringValueCStr(key);
97
+ env_value = getenv(env_key);
98
+
99
+ if (env_value == NULL) {
100
+ if (rb_block_given_p()) {
101
+ return rb_yield(key);
102
+ } else if (argc == 2) {
103
+ return default_value;
104
+ } else {
105
+ rb_raise(rb_eKeyError, "key not found: \"%s\"", env_key);
106
+ }
107
+ }
108
+
109
+ return rb_enc_interned_str_cstr(env_value, rb_locale_encoding());
110
+ }
111
+
112
+ /*
113
+ * call-seq:
114
+ * Arctic.key?(key) -> true or false
115
+ * Arctic.has_key?(key) -> true or false
116
+ * Arctic.include?(key) -> true or false
117
+ * Arctic.member?(key) -> true or false
118
+ *
119
+ * Returns +true+ if the environment variable +key+ exists, +false+ otherwise.
120
+ *
121
+ * This method has multiple aliases following Ruby's ENV API conventions.
122
+ *
123
+ * @param self [VALUE] The Arctic module
124
+ * @param key [VALUE] A String containing the environment variable name
125
+ * @return [VALUE] Qtrue if the key exists, Qfalse otherwise
126
+ *
127
+ * Example:
128
+ * Arctic.key?("PATH") #=> true
129
+ * Arctic.has_key?("XYZ") #=> false
130
+ */
131
+ static VALUE arctic_has_key(VALUE self, VALUE key) {
132
+ const char* env_key;
133
+
134
+ Check_Type(key, T_STRING);
135
+ env_key = StringValueCStr(key);
136
+
137
+ return getenv(env_key) != NULL ? Qtrue : Qfalse;
138
+ }
139
+
140
+ /*
141
+ * call-seq:
142
+ * Arctic.each {|key, value| block } -> Arctic
143
+ * Arctic.each -> Enumerator
144
+ * Arctic.each_pair {|key, value| block } -> Arctic
145
+ * Arctic.each_pair -> Enumerator
146
+ *
147
+ * Iterates over all environment variables.
148
+ *
149
+ * When called with a block, yields each environment variable as a [key, value]
150
+ * pair where both key and value are frozen, deduplicated strings. Returns Arctic.
151
+ *
152
+ * When called without a block, returns an Enumerator.
153
+ *
154
+ * @param self [VALUE] The Arctic module
155
+ * @return [VALUE] Arctic module (when block given) or Enumerator (when no block)
156
+ * @yield [key, value] Gives each environment variable name and value
157
+ *
158
+ * Example:
159
+ * Arctic.each { |key, value| puts "#{key}=#{value}" }
160
+ * Arctic.each.first(5) #=> [["HOME", "/Users/..."], ...]
161
+ */
162
+ static VALUE arctic_each(VALUE self) {
163
+ extern char **environ;
164
+ char **env = environ;
165
+
166
+ RETURN_SIZED_ENUMERATOR(self, 0, 0, env_count);
167
+
168
+ while (*env != NULL) {
169
+ char *entry = *env;
170
+ char *sep = strchr(entry, '=');
171
+
172
+ if (sep != NULL) {
173
+ VALUE key = rb_enc_interned_str(entry, sep - entry, rb_locale_encoding());
174
+ VALUE val = rb_enc_interned_str_cstr(sep + 1, rb_locale_encoding());
175
+ rb_yield_values(2, key, val);
176
+ }
177
+
178
+ env++;
179
+ }
180
+
181
+ return self;
182
+ }
183
+
184
+ /*
185
+ * call-seq:
186
+ * Arctic.keys -> Array
187
+ *
188
+ * Returns an array containing all environment variable names.
189
+ *
190
+ * Each key is a frozen, deduplicated string for memory efficiency.
191
+ *
192
+ * @param self [VALUE] The Arctic module
193
+ * @return [VALUE] An Array of frozen String keys
194
+ *
195
+ * Example:
196
+ * Arctic.keys #=> ["HOME", "PATH", "USER", ...]
197
+ */
198
+ static VALUE arctic_keys(VALUE self) {
199
+ extern char **environ;
200
+ char **env = environ;
201
+ VALUE ary = rb_ary_new();
202
+
203
+ while (*env != NULL) {
204
+ char *entry = *env;
205
+ char *sep = strchr(entry, '=');
206
+
207
+ if (sep != NULL) {
208
+ VALUE key = rb_enc_interned_str(entry, sep - entry, rb_locale_encoding());
209
+ rb_ary_push(ary, key);
210
+ }
211
+
212
+ env++;
213
+ }
214
+
215
+ return ary;
216
+ }
217
+
218
+ /*
219
+ * call-seq:
220
+ * Arctic.values -> Array
221
+ *
222
+ * Returns an array containing all environment variable values.
223
+ *
224
+ * Each value is a frozen, deduplicated string for memory efficiency.
225
+ *
226
+ * @param self [VALUE] The Arctic module
227
+ * @return [VALUE] An Array of frozen String values
228
+ *
229
+ * Example:
230
+ * Arctic.values #=> ["/Users/...", "/usr/bin:/bin", "username", ...]
231
+ */
232
+ static VALUE arctic_values(VALUE self) {
233
+ extern char **environ;
234
+ char **env = environ;
235
+ VALUE ary = rb_ary_new();
236
+
237
+ while (*env != NULL) {
238
+ char *entry = *env;
239
+ char *sep = strchr(entry, '=');
240
+
241
+ if (sep != NULL) {
242
+ VALUE val = rb_enc_interned_str_cstr(sep + 1, rb_locale_encoding());
243
+ rb_ary_push(ary, val);
244
+ }
245
+
246
+ env++;
247
+ }
248
+
249
+ return ary;
250
+ }
251
+
252
+ /*
253
+ * call-seq:
254
+ * Arctic.to_h -> Hash
255
+ *
256
+ * Returns a Hash containing all environment variables.
257
+ *
258
+ * Both keys and values are frozen, deduplicated strings for memory efficiency.
259
+ *
260
+ * @param self [VALUE] The Arctic module
261
+ * @return [VALUE] A Hash mapping environment variable names to their values
262
+ *
263
+ * Example:
264
+ * Arctic.to_h #=> {"HOME"=>"/Users/...", "PATH"=>"/usr/bin:/bin", ...}
265
+ */
266
+ static VALUE arctic_to_h(VALUE self) {
267
+ extern char **environ;
268
+ char **env = environ;
269
+ VALUE hash = rb_hash_new();
270
+
271
+ while (*env != NULL) {
272
+ char *entry = *env;
273
+ char *sep = strchr(entry, '=');
274
+
275
+ if (sep != NULL) {
276
+ VALUE key = rb_enc_interned_str(entry, sep - entry, rb_locale_encoding());
277
+ VALUE val = rb_enc_interned_str_cstr(sep + 1, rb_locale_encoding());
278
+ rb_hash_aset(hash, key, val);
279
+ }
280
+
281
+ env++;
282
+ }
283
+
284
+ return hash;
285
+ }
286
+
287
+ /*
288
+ * call-seq:
289
+ * Arctic.to_hash -> Hash
290
+ *
291
+ * Alias for Arctic.to_h.
292
+ *
293
+ * Returns a Hash containing all environment variables with frozen,
294
+ * deduplicated keys and values.
295
+ *
296
+ * @param self [VALUE] The Arctic module
297
+ * @return [VALUE] A Hash mapping environment variable names to their values
298
+ *
299
+ * @see arctic_to_h
300
+ */
301
+ static VALUE arctic_to_hash(VALUE self) {
302
+ return arctic_to_h(self);
303
+ }
304
+
305
+ /*
306
+ * call-seq:
307
+ * Arctic.empty? -> true or false
308
+ *
309
+ * Returns +true+ if the environment contains no variables, +false+ otherwise.
310
+ *
311
+ * This is a fast check that only examines the first element of the environ array.
312
+ *
313
+ * @param self [VALUE] The Arctic module
314
+ * @return [VALUE] Qtrue if environment is empty, Qfalse otherwise
315
+ *
316
+ * Example:
317
+ * Arctic.empty? #=> false (typically)
318
+ */
319
+ static VALUE arctic_empty_p(VALUE self) {
320
+ extern char **environ;
321
+ return environ[0] == NULL ? Qtrue : Qfalse;
322
+ }
323
+
324
+ /*
325
+ * call-seq:
326
+ * Arctic.size -> Integer
327
+ * Arctic.length -> Integer
328
+ *
329
+ * Returns the number of environment variables.
330
+ *
331
+ * This method is aliased as both +size+ and +length+ for compatibility with
332
+ * Ruby collection conventions.
333
+ *
334
+ * @param self [VALUE] The Arctic module
335
+ * @return [VALUE] An Integer representing the count of environment variables
336
+ *
337
+ * Example:
338
+ * Arctic.size #=> 42
339
+ * Arctic.length #=> 42
340
+ */
341
+ static VALUE arctic_size(VALUE self) {
342
+ return INT2NUM(env_count());
343
+ }
344
+
345
+ /*
346
+ * call-seq:
347
+ * Arctic.inspect -> String
348
+ *
349
+ * Returns a string representation of the Arctic module.
350
+ *
351
+ * The format is "#<Arctic:0x...>" where the hex value is the module's memory address.
352
+ *
353
+ * @param self [VALUE] The Arctic module
354
+ * @return [VALUE] A String containing the inspection representation
355
+ *
356
+ * Example:
357
+ * Arctic.inspect #=> "#<Arctic:0x00007f8b1c000000>"
358
+ */
359
+ static VALUE arctic_inspect(VALUE self) {
360
+ return rb_sprintf("#<Arctic:%p>", (void*)self);
361
+ }
362
+
363
+ /*
364
+ * Document-module: Arctic
365
+ *
366
+ * Arctic provides a read-only, memory-efficient interface to environment variables.
367
+ *
368
+ * Arctic is designed as a drop-in replacement for Ruby's ENV that prioritizes memory
369
+ * efficiency through aggressive string deduplication. All strings returned by Arctic
370
+ * are frozen and interned using rb_enc_interned_str_cstr(), which means repeated
371
+ * accesses to the same environment variable will return the exact same String object
372
+ * in memory rather than creating new allocations.
373
+ *
374
+ * == Key Features
375
+ *
376
+ * * *Read-only access*: Arctic only provides methods for reading environment variables,
377
+ * preventing accidental modifications to the environment.
378
+ * * *Memory efficient*: All returned strings are frozen and deduplicated, dramatically
379
+ * reducing memory usage when environment variables are accessed multiple times.
380
+ * * *ENV-compatible API*: Implements the most commonly-used read-only methods from Ruby's
381
+ * ENV, making it easy to switch between the two.
382
+ * * *Fast*: Direct C implementation with minimal overhead.
383
+ *
384
+ * == Usage
385
+ *
386
+ * # Basic access
387
+ * Arctic["PATH"] #=> "/usr/bin:/bin"
388
+ * Arctic["MISSING"] #=> nil
389
+ *
390
+ * # Fetch with defaults or blocks
391
+ * Arctic.fetch("HOME") #=> "/Users/username"
392
+ * Arctic.fetch("MISSING", "default") #=> "default"
393
+ * Arctic.fetch("MISSING") { |k| "#{k}?" } #=> "MISSING?"
394
+ *
395
+ * # Existence checks
396
+ * Arctic.key?("PATH") #=> true
397
+ * Arctic.has_key?("XYZ") #=> false
398
+ *
399
+ * # Iteration
400
+ * Arctic.each { |k, v| puts "#{k}=#{v}" }
401
+ * Arctic.keys #=> ["HOME", "PATH", "USER", ...]
402
+ * Arctic.values #=> ["/Users/...", "/usr/bin:/bin", ...]
403
+ *
404
+ * # Conversion
405
+ * Arctic.to_h #=> {"HOME"=>"/Users/...", "PATH"=>...}
406
+ *
407
+ * # Information
408
+ * Arctic.size #=> 42
409
+ * Arctic.empty? #=> false
410
+ *
411
+ * == Memory Efficiency
412
+ *
413
+ * Consider a typical Rails application that might access ENV["RAILS_ENV"] hundreds
414
+ * or thousands of times during initialization. With standard ENV, each access creates
415
+ * a new String object. With Arctic, all accesses return the exact same frozen String
416
+ * object, using only a single allocation regardless of how many times it's accessed.
417
+ *
418
+ * This is especially valuable in large applications with many environment variables
419
+ * or in long-running processes that frequently access configuration from the environment.
420
+ *
421
+ * == Comparison with ENV
422
+ *
423
+ * Arctic implements the following ENV methods:
424
+ * - Arctic[] (like ENV[])
425
+ * - Arctic.fetch (like ENV.fetch)
426
+ * - Arctic.key?, has_key?, include?, member? (like ENV.key? etc.)
427
+ * - Arctic.each, each_pair (like ENV.each)
428
+ * - Arctic.keys, values (like ENV.keys, ENV.values)
429
+ * - Arctic.to_h, to_hash (like ENV.to_h)
430
+ * - Arctic.empty?, size, length (like ENV.empty?, ENV.size)
431
+ *
432
+ * Arctic does NOT implement write operations like []=, store, update, delete, clear, etc.
433
+ * For modifying the environment, continue using ENV.
434
+ *
435
+ * @see https://docs.ruby-lang.org/en/master/ENV.html ENV documentation for comparison
436
+ */
437
+
438
+ /*
439
+ * Initializes the Arctic extension.
440
+ *
441
+ * This function is called by Ruby when the extension is loaded. It defines
442
+ * the Arctic module and registers all its singleton methods.
443
+ *
444
+ * The Arctic module provides a read-only, memory-efficient interface to
445
+ * environment variables, using frozen and deduplicated strings throughout.
446
+ *
447
+ * Registered methods include:
448
+ * - Core access: [], fetch
449
+ * - Existence checks: key?, has_key?, include?, member?
450
+ * - Iteration: each, each_pair
451
+ * - Conversion: keys, values, to_h, to_hash
452
+ * - Information: empty?, size, length
453
+ * - Utility: inspect
454
+ */
455
+ void Init_arctic(void) {
456
+ mArctic = rb_define_module("Arctic");
457
+
458
+ // Core access methods
459
+ rb_define_singleton_method(mArctic, "[]", arctic_aref, 1);
460
+ rb_define_singleton_method(mArctic, "fetch", arctic_fetch, -1);
461
+
462
+ // Existence checks (multiple aliases per ENV API)
463
+ rb_define_singleton_method(mArctic, "key?", arctic_has_key, 1);
464
+ rb_define_singleton_method(mArctic, "has_key?", arctic_has_key, 1);
465
+ rb_define_singleton_method(mArctic, "include?", arctic_has_key, 1);
466
+ rb_define_singleton_method(mArctic, "member?", arctic_has_key, 1);
467
+
468
+ // Iteration
469
+ rb_define_singleton_method(mArctic, "each", arctic_each, 0);
470
+ rb_define_singleton_method(mArctic, "each_pair", arctic_each, 0);
471
+
472
+ // Conversion methods
473
+ rb_define_singleton_method(mArctic, "keys", arctic_keys, 0);
474
+ rb_define_singleton_method(mArctic, "values", arctic_values, 0);
475
+ rb_define_singleton_method(mArctic, "to_h", arctic_to_h, 0);
476
+ rb_define_singleton_method(mArctic, "to_hash", arctic_to_hash, 0);
477
+
478
+ // Info methods
479
+ rb_define_singleton_method(mArctic, "empty?", arctic_empty_p, 0);
480
+ rb_define_singleton_method(mArctic, "size", arctic_size, 0);
481
+ rb_define_singleton_method(mArctic, "length", arctic_size, 0);
482
+
483
+ // Utility methods
484
+ rb_define_singleton_method(mArctic, "inspect", arctic_inspect, 0);
485
+ }
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ require 'mkmf'
5
+
6
+ # Check for required headers
7
+ have_header('stdlib.h') or raise 'stdlib.h not found'
8
+ have_header('string.h') or raise 'string.h not found'
9
+
10
+ # Check for interned string functions (available in Ruby 2.3+)
11
+ have_func('rb_interned_str_cstr', 'ruby.h')
12
+ have_func('rb_enc_interned_str_cstr', 'ruby.h')
13
+ have_func('rb_enc_interned_str', 'ruby.h')
14
+ have_func('rb_locale_encoding', 'ruby/encoding.h')
15
+
16
+ # Create Makefile for arctic extension
17
+ create_makefile('arctic/arctic')
data/lib/arctic.rb ADDED
@@ -0,0 +1,16 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ module Arctic
5
+ VERSION = "0.1.0"
6
+ end
7
+
8
+ # Load the compiled C extension
9
+ # The extension defines the Arctic module and all its methods
10
+ begin
11
+ require 'arctic/arctic'
12
+ rescue LoadError => e
13
+ # Provide helpful error message if extension not compiled
14
+ raise LoadError, 'Could not load arctic extension. ' \
15
+ "Original error: #{e.message}"
16
+ end
data/sig/arctic.rbs ADDED
@@ -0,0 +1,111 @@
1
+ # TypeProf 0.21.11
2
+
3
+ # Arctic provides a read-only, memory-efficient interface to environment variables.
4
+ #
5
+ # All returned strings are frozen and deduplicated using Ruby's fstring table,
6
+ # dramatically reducing memory usage when environment variables are accessed multiple times.
7
+ module Arctic
8
+ # The version of the Arctic gem
9
+ VERSION: String
10
+
11
+ # Retrieves the value of the environment variable +key+.
12
+ #
13
+ # Returns a frozen, deduplicated string. If the environment variable does not exist, returns nil.
14
+ #
15
+ # @param key [String] The environment variable name
16
+ # @return [String, nil] The frozen string value of the environment variable, or nil if not found
17
+ def self.[]: (String key) -> String?
18
+
19
+ # Retrieves the environment variable +key+.
20
+ #
21
+ # If the key exists, returns its frozen, deduplicated value.
22
+ # If the key does not exist:
23
+ # - With a block: yields the key to the block and returns the block's result
24
+ # - With a default argument: returns the default value
25
+ # - Without either: raises KeyError
26
+ #
27
+ # @param key [String] The environment variable name
28
+ # @return [String] The environment variable value
29
+ # @raise [KeyError] If the key is not found and no default or block is provided
30
+ def self.fetch: (String key) -> String
31
+ | [T] (String key, T default) -> (String | T)
32
+ | [T] (String key) { (String) -> T } -> (String | T)
33
+
34
+ # Returns true if the environment variable +key+ exists, false otherwise.
35
+ #
36
+ # @param key [String] The environment variable name
37
+ # @return [bool] true if the key exists, false otherwise
38
+ def self.key?: (String key) -> bool
39
+
40
+ # Alias for key?
41
+ alias self.has_key? self.key?
42
+
43
+ # Alias for key?
44
+ alias self.include? self.key?
45
+
46
+ # Alias for key?
47
+ alias self.member? self.key?
48
+
49
+ # Iterates over all environment variables.
50
+ #
51
+ # When called with a block, yields each environment variable as a [key, value]
52
+ # pair where both key and value are frozen, deduplicated strings. Returns Arctic.
53
+ #
54
+ # When called without a block, returns an Enumerator.
55
+ #
56
+ # @return [Arctic, Enumerator] Arctic module (when block given) or Enumerator (when no block)
57
+ # @yield [key, value] Gives each environment variable name and value
58
+ def self.each: () { (String key, String value) -> void } -> self
59
+ | () -> Enumerator[[String, String], self]
60
+
61
+ # Alias for each
62
+ #
63
+ # @return [Arctic, Enumerator] Arctic module (when block given) or Enumerator (when no block)
64
+ # @yield [key, value] Gives each environment variable name and value
65
+ def self.each_pair: () { (String key, String value) -> void } -> self
66
+ | () -> Enumerator[[String, String], self]
67
+
68
+ # Returns an array containing all environment variable names.
69
+ #
70
+ # Each key is a frozen, deduplicated string for memory efficiency.
71
+ #
72
+ # @return [Array<String>] An Array of frozen String keys
73
+ def self.keys: () -> Array[String]
74
+
75
+ # Returns an array containing all environment variable values.
76
+ #
77
+ # Each value is a frozen, deduplicated string for memory efficiency.
78
+ #
79
+ # @return [Array<String>] An Array of frozen String values
80
+ def self.values: () -> Array[String]
81
+
82
+ # Returns a Hash containing all environment variables.
83
+ #
84
+ # Both keys and values are frozen, deduplicated strings for memory efficiency.
85
+ #
86
+ # @return [Hash<String, String>] A Hash mapping environment variable names to their values
87
+ def self.to_h: () -> Hash[String, String]
88
+
89
+ # Alias for to_h
90
+ #
91
+ # @return [Hash<String, String>] A Hash mapping environment variable names to their values
92
+ alias self.to_hash self.to_h
93
+
94
+ # Returns true if the environment contains no variables, false otherwise.
95
+ #
96
+ # @return [bool] true if environment is empty, false otherwise
97
+ def self.empty?: () -> bool
98
+
99
+ # Returns the number of environment variables.
100
+ #
101
+ # @return [Integer] The count of environment variables
102
+ def self.size: () -> Integer
103
+
104
+ # Alias for size
105
+ alias self.length self.size
106
+
107
+ # Returns a string representation of the Arctic module.
108
+ #
109
+ # @return [String] A String containing the inspection representation
110
+ def self.inspect: () -> String
111
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: arctic
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Persona
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2026-01-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Arctic provides an ENV-like interface that returns frozen, deduplicated
14
+ strings to reduce memory allocations. Implemented as a C extension using Ruby's
15
+ fstring table.
16
+ email:
17
+ - rubygems@withpersona.com
18
+ executables: []
19
+ extensions:
20
+ - ext/arctic/extconf.rb
21
+ extra_rdoc_files: []
22
+ files:
23
+ - CHANGELOG.md
24
+ - LICENSE.txt
25
+ - README.md
26
+ - ext/arctic/arctic.c
27
+ - ext/arctic/extconf.rb
28
+ - lib/arctic.rb
29
+ - sig/arctic.rbs
30
+ homepage: https://github.com/persona-id/arctic
31
+ licenses:
32
+ - MIT
33
+ metadata:
34
+ homepage_uri: https://github.com/persona-id/arctic
35
+ source_code_uri: https://github.com/persona-id/arctic
36
+ changelog_uri: https://github.com/persona-id/arctic/blob/main/CHANGELOG.md
37
+ rubygems_mfa_required: 'true'
38
+ post_install_message:
39
+ rdoc_options: []
40
+ require_paths:
41
+ - lib
42
+ required_ruby_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '3.3'
47
+ required_rubygems_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ requirements: []
53
+ rubygems_version: 3.5.22
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Frozen, deduplicated environment variable access
57
+ test_files: []