invar 0.8.0 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4826c528fc5940bcc955a1fc762abc2b4800a30eb4ce5b34af0ea313dd990ab
4
- data.tar.gz: f463ed247adc47ea89d7b6965cd427d21a98de125271b262541eb0703a792d62
3
+ metadata.gz: 2346259fc26689ffca5386f39e982de60dc9f2d9c03399f85023e05a47802f7a
4
+ data.tar.gz: b6e2cea0c52fd0b52eb833036844c03d3a714d83e0572dc5dcbb4ae083c61cf7
5
5
  SHA512:
6
- metadata.gz: 6bbc1f4df04c3f4917a2cfcce611acde0859ccd74b412c0b9faa021770c4f01d246defb0131fc8052d0e2aeb40d904d40a043ecccb7a872f966b4a9264e812a3
7
- data.tar.gz: 32a1754130909b04d488adb88b67e5efe193c2173103d9041338652026ef45a55b3e4fbaa49bed2863f8b5d0d70a257ace52b5bbff913236f4c82b4a90af06bf
6
+ metadata.gz: 1a9556b30d2c17fda9373551ecf6fffeaacabd35aa0e40bb883c78e8bd94642bf4bebbeea054b5f9a9681b0bb23b6ee8c30860afb732f94b9d7887e62e0c0c47
7
+ data.tar.gz: a80954a8dbfc4c42d3f57b94cb2830a8e598eda85cd95f37ff431a1ab2b253fa64d4c65f9aacba8cfa718f5b5f1082b05f78c0d1f97d2ae7bc58b4392c4e19f0
data/.simplecov ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ SimpleCov.start do
4
+ coverage_dir '.coverage'
5
+ enable_coverage :branch
6
+
7
+ root __dir__
8
+ end
data/README.md CHANGED
@@ -26,7 +26,8 @@ variables have some downsides:
26
26
  * Cannot be easily checked against a schema for early error detection
27
27
  * Ruby's core ENV does not accept symbols as keys (a minor nuisance, but it counts)
28
28
 
29
- > **Fun Fact:** Invar is named for an [alloy used in clockmaking](https://en.wikipedia.org/wiki/Invar) - it's short for "**invar**iable".
29
+ > **Fun Fact:** Invar is named for an [alloy used in clockmaking](https://en.wikipedia.org/wiki/Invar) - it's short
30
+ > for "**invar**iable".
30
31
 
31
32
  ### Features
32
33
 
@@ -216,6 +217,10 @@ To open the file your system's default text editor (eg. nano):
216
217
 
217
218
  bundle exec rake invar:config
218
219
 
220
+ > **Automation tip** You can provide new content over STDIN
221
+ >
222
+ > bundle exec rake invar:config < config_template.yml
223
+
219
224
  #### Edit Secrets File
220
225
 
221
226
  To edit the secrets file, run this and provide the file's encryption key:
@@ -223,7 +228,27 @@ To edit the secrets file, run this and provide the file's encryption key:
223
228
  bundle exec rake invar:secrets
224
229
 
225
230
  The file will be decrypted and opened in your default editor like the config file. Once you have exited the editor, it
226
- will be re-encrypted. Remember to save your changes!
231
+ will be re-encrypted. **Remember to save your changes!**
232
+
233
+ > **Automation tip** You can set the current encryption key in the `LOCKBOX_MASTER_KEY` environment variable:
234
+ >
235
+ > LOCKBOX_MASTER_KEY=sooper_sekret_key_here bundle exec rake invar:secrets
236
+
237
+ > **Automation tip** Like invar:config, you can provide new content over STDIN
238
+ >
239
+ > bundle exec rake invar:secrets < secrets_template.yml
240
+
241
+ #### Rotating the Secrets File Encryption Key
242
+
243
+ To re-encrypt the secrets file, run this and provide the file's current encryption key:
244
+
245
+ bundle exec rake invar:rotate
246
+
247
+ A new encryption key will be generated just like using `invar:init`.
248
+
249
+ > **Automation tip** you can set the current encryption key in the `LOCKBOX_MASTER_KEY` environment variable:
250
+ >
251
+ > LOCKBOX_MASTER_KEY=sooper_sekret_key_here bundle exec rake invar:rotate
227
252
 
228
253
  ### Code
229
254
 
data/RELEASE_NOTES.md CHANGED
@@ -19,6 +19,36 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
19
19
 
20
20
  * none
21
21
 
22
+ ## [0.9.1] - 2023-09-30
23
+
24
+ ### Major Changes
25
+
26
+ * none
27
+
28
+ ### Minor Changes
29
+
30
+ * none
31
+
32
+ ### Bugfixes
33
+
34
+ * Improved handling of test-only methods when trying to access them via `#method`
35
+ * Fixed TTY detection
36
+
37
+ ## [0.9.0] - 2023-09-24
38
+
39
+ ### Major Changes
40
+
41
+ * none
42
+
43
+ ### Minor Changes
44
+
45
+ * Added Rake task for secrets file key rotation
46
+ * Added ability for invar:config and invar:secrets to take replacement content over `STDIN` instead of live editing
47
+
48
+ ### Bugfixes
49
+
50
+ * none
51
+
22
52
  ## [0.8.0] - 2023-09-13
23
53
 
24
54
  ### Major Changes
@@ -9,7 +9,7 @@ module Invar
9
9
  # Verifies a file is secure
10
10
  class PrivateFile
11
11
  extend Forwardable
12
- def_delegators :@delegate_sd_obj, :stat, :to_s, :basename, :==, :chmod
12
+ def_delegators :@delegate_sd_obj, :stat, :to_s, :basename, :dirname, :extname, :==, :chmod, :rename, :write
13
13
 
14
14
  # Mask for limiting to the lowest three octal digits (which store permissions)
15
15
  PERMISSIONS_MASK = 0o777
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'namespaced'
4
+
5
+ module Invar
6
+ module Rake
7
+ module Task
8
+ # Rake task handler for actions to do with configuration files
9
+ class ConfigFileHandler < NamespacedFileTask
10
+ # Creates a config file in the appropriate location
11
+ def create
12
+ config_dir.mkpath
13
+ file_path.write CONFIG_TEMPLATE
14
+ file_path.chmod 0o600
15
+
16
+ warn "Created file: #{ file_path }"
17
+ end
18
+
19
+ # Edits the existing config file in the appropriate location
20
+ def edit
21
+ content = $stdin.stat.pipe? ? $stdin.read : nil
22
+ file_path = configs_file
23
+
24
+ if content
25
+ file_path.write content
26
+ else
27
+ system ENV.fetch('EDITOR', 'editor'), file_path.to_s, exception: true
28
+ end
29
+
30
+ warn "File saved to: #{ file_path }"
31
+ end
32
+
33
+ private
34
+
35
+ def configs_file
36
+ @locator.find 'config.yml'
37
+ rescue ::Invar::FileLocator::FileNotFoundError => e
38
+ warn <<~ERR
39
+ Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
40
+ #{ CREATE_SUGGESTION }
41
+ ERR
42
+ exit 1
43
+ end
44
+
45
+ def filename
46
+ 'config.yml'
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../tasks'
4
+
5
+ module Invar
6
+ module Rake
7
+ module Task
8
+ # Abstract class for tasks that use a namespace for file searching
9
+ class NamespacedFileTask
10
+ def initialize(namespace)
11
+ @locator = FileLocator.new(namespace)
12
+ end
13
+
14
+ def file_path
15
+ config_dir / filename
16
+ end
17
+
18
+ private
19
+
20
+ def config_dir
21
+ @locator.search_paths.first
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'namespaced'
4
+
5
+ module Invar
6
+ module Rake
7
+ module Task
8
+ # Rake task handler for actions on the secrets file.
9
+ class SecretsFileHandler < NamespacedFileTask
10
+ # Instructions hint for how to handle secret keys.
11
+ SECRETS_INSTRUCTIONS = <<~INST
12
+ Generated key. Save this key to a secure password manager, you will need it to edit the secrets.yml file:
13
+ INST
14
+
15
+ SWAP_EXT = 'tmp'
16
+
17
+ # Creates a new encrypted secrets file and prints the generated encryption key to STDOUT
18
+ def create(content: SECRETS_TEMPLATE)
19
+ encryption_key = Lockbox.generate_key
20
+
21
+ write_encrypted_file(file_path,
22
+ encryption_key: encryption_key,
23
+ content: content,
24
+ permissions: PrivateFile::DEFAULT_PERMISSIONS)
25
+
26
+ warn SECRETS_INSTRUCTIONS
27
+ puts encryption_key
28
+ end
29
+
30
+ # Updates the file with new content.
31
+ #
32
+ # Either the content is provided over STDIN or the default editor is opened with the decrypted contents of
33
+ # the secrets file. After closing the editor, the file will be updated with the new encrypted contents.
34
+ def edit
35
+ content = $stdin.stat.pipe? ? $stdin.read : nil
36
+
37
+ edit_encrypted_file(secrets_file, content: content)
38
+
39
+ warn "File saved to #{ secrets_file }"
40
+ end
41
+
42
+ def rotate
43
+ file_path = secrets_file
44
+
45
+ decrypted = read_encrypted_file(file_path, encryption_key: determine_key(file_path))
46
+
47
+ swap_file = file_path.dirname / [file_path.basename, SWAP_EXT].join('.')
48
+ file_path.rename swap_file
49
+
50
+ begin
51
+ create content: decrypted
52
+ swap_file.delete
53
+ rescue StandardError
54
+ swap_file.rename file_path.to_s
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def secrets_file
61
+ @locator.find 'secrets.yml'
62
+ rescue ::Invar::FileLocator::FileNotFoundError => e
63
+ warn <<~ERR
64
+ Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
65
+ #{ CREATE_SUGGESTION }
66
+ ERR
67
+ exit 1
68
+ end
69
+
70
+ def filename
71
+ 'secrets.yml'
72
+ end
73
+
74
+ def read_encrypted_file(file_path, encryption_key:)
75
+ lockbox = build_lockbox(encryption_key)
76
+ lockbox.decrypt(file_path.binread)
77
+ end
78
+
79
+ def write_encrypted_file(file_path, encryption_key:, content:, permissions: nil)
80
+ lockbox = build_lockbox(encryption_key)
81
+
82
+ encrypted_data = lockbox.encrypt(content)
83
+
84
+ config_dir.mkpath
85
+ # TODO: replace File.opens with photo_path.binwrite(uri.data) once FakeFS can handle it
86
+ File.open(file_path.to_s, 'wb') { |f| f.write encrypted_data }
87
+ file_path.chmod permissions if permissions
88
+
89
+ warn "Saved file: #{ file_path }"
90
+ end
91
+
92
+ def edit_encrypted_file(file_path, content: nil)
93
+ encryption_key = determine_key(file_path)
94
+
95
+ content ||= invoke_editor(file_path, encryption_key: encryption_key)
96
+
97
+ write_encrypted_file(file_path, encryption_key: encryption_key, content: content)
98
+ end
99
+
100
+ def invoke_editor(file_path, encryption_key:)
101
+ Tempfile.create(file_path.basename.to_s) do |tmp_file|
102
+ decrypted = read_encrypted_file(file_path, encryption_key: encryption_key)
103
+
104
+ tmp_file.write(decrypted)
105
+ tmp_file.rewind # rewind needed because file does not get closed after write
106
+ system ENV.fetch('EDITOR', 'editor'), tmp_file.path, exception: true
107
+ tmp_file.read
108
+ end
109
+ end
110
+
111
+ def determine_key(file_path)
112
+ encryption_key = Lockbox.master_key
113
+
114
+ if encryption_key.nil? && $stdin.tty?
115
+ warn "Enter master key to decrypt #{ file_path }:"
116
+ encryption_key = $stdin.noecho(&:gets).strip
117
+ end
118
+
119
+ encryption_key
120
+ end
121
+
122
+ def build_lockbox(encryption_key)
123
+ Lockbox.new(key: encryption_key)
124
+ rescue ArgumentError => e
125
+ raise SecretsFileEncryptionError, e
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'namespaced'
4
+
5
+ module Invar
6
+ module Rake
7
+ module Task
8
+ # Rake task handler for actions that just show information about the system
9
+ class StatusHandler < NamespacedFileTask
10
+ # Prints the current paths to be searched in
11
+ def show_paths
12
+ warn @locator.search_paths.join("\n")
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -3,29 +3,24 @@
3
3
  require 'invar'
4
4
 
5
5
  require 'rake'
6
- require 'io/console'
7
6
  require 'tempfile'
8
7
 
8
+ require_relative 'task/config'
9
+ require_relative 'task/secrets'
10
+ require_relative 'task/status'
11
+
9
12
  module Invar
10
- # Rake task implementation.
13
+ # Rake task module for Invar-related tasks.
14
+ #
15
+ # The specific rake task implementations are delegated to handlers in Invar::Rake::Task
11
16
  #
12
- # The actual rake tasks themselves are thinly defined in invar/rake.rb (so that the external include
13
- # path is nice and short)
17
+ # @see Invar::Rake::Tasks.define
14
18
  module Rake
15
19
  # RakeTask builder class. Use Tasks.define to generate the needed tasks.
16
20
  class Tasks
17
21
  include ::Rake::Cloneable
18
22
  include ::Rake::DSL
19
23
 
20
- # Template config YAML file
21
- CONFIG_TEMPLATE = SECRETS_TEMPLATE = <<~YML
22
- ---
23
- YML
24
-
25
- CREATE_SUGGESTION = <<~SUGGESTION
26
- Maybe you used the wrong namespace or need to create the file with bundle exec rake invar:init?
27
- SUGGESTION
28
-
29
24
  # Shorthand for Invar::Rake::Tasks.new.define
30
25
  #
31
26
  # @param (see #define)
@@ -51,7 +46,7 @@ module Invar
51
46
  define_init_task(app_namespace)
52
47
 
53
48
  define_config_task(app_namespace)
54
- define_secrets_task(app_namespace)
49
+ define_secrets_tasks(app_namespace)
55
50
 
56
51
  define_info_tasks(app_namespace)
57
52
  end
@@ -62,8 +57,8 @@ module Invar
62
57
  task :init, [:mode] do |_task, args|
63
58
  mode = args.mode
64
59
 
65
- config = ::Invar::Rake::Tasks::ConfigTask.new(app_namespace)
66
- secrets = ::Invar::Rake::Tasks::SecretTask.new(app_namespace)
60
+ config = ::Invar::Rake::Task::ConfigFileHandler.new(app_namespace)
61
+ secrets = ::Invar::Rake::Task::SecretsFileHandler.new(app_namespace)
67
62
 
68
63
  case mode
69
64
  when 'config'
@@ -71,7 +66,7 @@ module Invar
71
66
  when 'secrets'
72
67
  config = nil
73
68
  else
74
- raise "unknown mode #{ mode }. Must be one of 'config' or 'secrets'" unless mode.nil?
69
+ raise ArgumentError, "unknown mode '#{ mode }'. Must be one of 'config' or 'secrets'" unless mode.nil?
75
70
  end
76
71
 
77
72
  assert_init_conditions(config&.file_path, secrets&.file_path)
@@ -84,39 +79,44 @@ module Invar
84
79
  def define_config_task(app_namespace)
85
80
  desc 'Edit the config in your default editor'
86
81
  task :configs do
87
- ::Invar::Rake::Tasks::ConfigTask.new(app_namespace).edit
82
+ ::Invar::Rake::Task::ConfigFileHandler.new(app_namespace).edit
88
83
  end
89
84
 
90
85
  # alias
91
86
  task config: ['configs']
92
87
  end
93
88
 
94
- def define_secrets_task(app_namespace)
89
+ def define_secrets_tasks(app_namespace)
95
90
  desc 'Edit the encrypted secrets file in your default editor'
96
91
  task :secrets do
97
- ::Invar::Rake::Tasks::SecretTask.new(app_namespace).edit
92
+ ::Invar::Rake::Task::SecretsFileHandler.new(app_namespace).edit
98
93
  end
99
94
 
100
95
  # alias
101
96
  task secret: ['secrets']
97
+
98
+ desc 'Encrypt the secrets file with a new generated key'
99
+ task :rotate do
100
+ ::Invar::Rake::Task::SecretsFileHandler.new(app_namespace).rotate
101
+ end
102
102
  end
103
103
 
104
104
  def define_info_tasks(app_namespace)
105
105
  desc 'Show directories to be searched for the given namespace'
106
106
  task :paths do
107
- ::Invar::Rake::Tasks::StateTask.new(app_namespace).show_paths
107
+ ::Invar::Rake::Task::StatusHandler.new(app_namespace).show_paths
108
108
  end
109
109
  end
110
110
 
111
111
  def assert_init_conditions(config_file, secrets_file)
112
112
  return unless config_file&.exist? || secrets_file&.exist?
113
113
 
114
- msg = if !config_file&.exist?
114
+ msg = if !config_file.exist?
115
115
  <<~MSG
116
116
  Abort: Secrets file already exists (#{ secrets_file })
117
117
  Run this to init only the config file: bundle exec rake tasks invar:init[config]
118
118
  MSG
119
- elsif !secrets_file&.exist?
119
+ elsif !secrets_file.exist?
120
120
  <<~MSG
121
121
  Abort: Config file already exists (#{ config_file })
122
122
  Run this to init only the secrets file: bundle exec rake tasks invar:init[secrets]
@@ -131,163 +131,18 @@ module Invar
131
131
  warn msg
132
132
  exit 1
133
133
  end
134
+ end
134
135
 
135
- # Tasks that use a namespace for file searching
136
- class NamespacedTask
137
- def initialize(namespace)
138
- @locator = FileLocator.new(namespace)
139
- end
140
-
141
- def file_path
142
- config_dir / filename
143
- end
144
-
145
- private
146
-
147
- def config_dir
148
- @locator.search_paths.first
149
- end
150
- end
151
-
152
- # Configuration file actions.
153
- class ConfigTask < NamespacedTask
154
- # Creates a config file in the appropriate location
155
- def create
156
- raise 'File already exists' if file_path.exist?
157
-
158
- config_dir.mkpath
159
- file_path.write CONFIG_TEMPLATE
160
- file_path.chmod 0o600
161
-
162
- warn "Created file: #{ file_path }"
163
- end
164
-
165
- # Edits the existing config file in the appropriate location
166
- def edit
167
- configs_file = begin
168
- @locator.find('config.yml')
169
- rescue ::Invar::FileLocator::FileNotFoundError => e
170
- warn <<~ERR
171
- Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
172
- #{ CREATE_SUGGESTION }
173
- ERR
174
- exit 1
175
- end
176
-
177
- system(ENV.fetch('EDITOR', 'editor'), configs_file.to_s, exception: true)
178
-
179
- warn "File saved to: #{ configs_file }"
180
- end
181
-
182
- private
183
-
184
- def filename
185
- 'config.yml'
186
- end
187
- end
188
-
189
- # Secrets file actions.
190
- class SecretTask < NamespacedTask
191
- # Instructions hint for how to handle secret keys.
192
- SECRETS_INSTRUCTIONS = <<~INST
193
- Save this key to a secure password manager. You will need it to edit the secrets.yml file.
194
- INST
195
-
196
- # Creates a new encrypted secrets file and prints the generated encryption key to STDOUT
197
- def create
198
- raise 'File already exists' if file_path.exist?
199
-
200
- encryption_key = Lockbox.generate_key
201
-
202
- write_encrypted_file(file_path,
203
- encryption_key: encryption_key,
204
- content: SECRETS_TEMPLATE,
205
- permissions: PrivateFile::DEFAULT_PERMISSIONS)
206
-
207
- warn "Created file: #{ file_path }"
208
-
209
- warn SECRETS_INSTRUCTIONS
210
- warn 'Generated key is:'
211
- puts encryption_key
212
- end
213
-
214
- # Opens an editor for the decrypted contents of the secrets file. After closing the editor, the file will be
215
- # updated with the new encrypted contents.
216
- def edit
217
- secrets_file = begin
218
- @locator.find('secrets.yml')
219
- rescue ::Invar::FileLocator::FileNotFoundError => e
220
- warn <<~ERR
221
- Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
222
- #{ CREATE_SUGGESTION }
223
- ERR
224
- exit 1
225
- end
226
-
227
- edit_encrypted_file(secrets_file)
228
-
229
- warn "File saved to #{ secrets_file }"
230
- end
231
-
232
- private
233
-
234
- def filename
235
- 'secrets.yml'
236
- end
237
-
238
- def write_encrypted_file(file_path, encryption_key:, content:, permissions: nil)
239
- lockbox = Lockbox.new(key: encryption_key)
240
-
241
- encrypted_data = lockbox.encrypt(content)
242
-
243
- config_dir.mkpath
244
- # TODO: replace File.opens with photo_path.binwrite(uri.data) once FakeFS can handle it
245
- File.open(file_path.to_s, 'wb') { |f| f.write encrypted_data }
246
- file_path.chmod permissions if permissions
247
- end
248
-
249
- def edit_encrypted_file(file_path)
250
- encryption_key = determine_key(file_path)
251
-
252
- lockbox = build_lockbox(encryption_key)
253
-
254
- file_str = Tempfile.create(file_path.basename.to_s) do |tmp_file|
255
- decrypted = lockbox.decrypt(file_path.binread)
256
-
257
- tmp_file.write(decrypted)
258
- tmp_file.rewind # rewind needed because file does not get closed after write
259
- system(ENV.fetch('EDITOR', 'editor'), tmp_file.path, exception: true)
260
- tmp_file.read
261
- end
262
-
263
- write_encrypted_file(file_path, encryption_key: encryption_key, content: file_str)
264
- end
265
-
266
- def determine_key(file_path)
267
- encryption_key = Lockbox.master_key
268
-
269
- if encryption_key.nil? && $stdin.respond_to?(:noecho)
270
- warn "Enter master key to decrypt #{ file_path }:"
271
- encryption_key = $stdin.noecho(&:gets).strip
272
- end
273
-
274
- encryption_key
275
- end
276
-
277
- def build_lockbox(encryption_key)
278
- Lockbox.new(key: encryption_key)
279
- rescue ArgumentError => e
280
- raise SecretsFileEncryptionError, e
281
- end
282
- end
136
+ # Namespace module for task handler implementations
137
+ module Task
138
+ # Template config YAML file
139
+ CONFIG_TEMPLATE = SECRETS_TEMPLATE = <<~YML
140
+ ---
141
+ YML
283
142
 
284
- # General status tasks
285
- class StateTask < NamespacedTask
286
- # Prints the current paths to be searched in
287
- def show_paths
288
- warn @locator.search_paths.join("\n")
289
- end
290
- end
143
+ CREATE_SUGGESTION = <<~SUGGESTION
144
+ Maybe you used the wrong namespace or need to create the file with bundle exec rake invar:init?
145
+ SUGGESTION
291
146
  end
292
147
  end
293
148
  end
data/lib/invar/reality.rb CHANGED
@@ -152,11 +152,9 @@ module Invar
152
152
  end
153
153
 
154
154
  def resolve_key(pathname, locator, prompt)
155
- key_file = locator.find(pathname)
156
-
157
- read_keyfile(key_file)
155
+ read_keyfile locator.find pathname
158
156
  rescue FileLocator::FileNotFoundError
159
- if $stdin.respond_to?(:noecho)
157
+ if $stdin.tty?
160
158
  warn prompt
161
159
  $stdin.noecho(&:gets).strip
162
160
  else
data/lib/invar/scope.rb CHANGED
@@ -25,12 +25,18 @@ module Invar
25
25
  alias / fetch
26
26
  alias [] fetch
27
27
 
28
- def method_missing(symbol, *args)
29
- raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::PRETEND_MSG if symbol == :pretend
28
+ def method_missing(method_name, *args)
29
+ guard_test_methods method_name
30
30
 
31
31
  super
32
32
  end
33
33
 
34
+ def respond_to_missing?(method_name, include_all)
35
+ guard_test_methods method_name
36
+
37
+ super method_name, include_all
38
+ end
39
+
34
40
  # Returns a hash representation of this scope and subscopes.
35
41
  #
36
42
  # @return [Hash] a hash representation of this scope
@@ -51,6 +57,10 @@ module Invar
51
57
 
52
58
  private
53
59
 
60
+ def guard_test_methods(method_name)
61
+ raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::PRETEND_MSG if method_name == :pretend
62
+ end
63
+
54
64
  def known_keys
55
65
  @data.keys.sort.collect { |k| ":#{ k }" }.join(', ')
56
66
  end
data/lib/invar/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Invar
4
4
  # Current version of the gem
5
- VERSION = '0.8.0'
5
+ VERSION = '0.9.1'
6
6
  end
data/lib/invar.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Needed for TTY input handling
4
+ # Do not remove; it missing will not always break tests due to test environments requiring it themselves
5
+ require 'io/console'
6
+
3
7
  require_relative 'invar/version'
4
8
  require_relative 'invar/errors'
5
9
  require_relative 'invar/reality'
@@ -15,12 +19,24 @@ module Invar
15
19
  end
16
20
 
17
21
  class << self
18
- def method_missing(meth)
19
- if [:after_load, :clear_hooks].include? meth
20
- raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::HOOK_MSG
21
- end
22
+ def method_missing(method_name)
23
+ guard_test_hooks method_name
22
24
 
23
25
  super
24
26
  end
27
+
28
+ def respond_to_missing?(method_name, include_all)
29
+ guard_test_hooks method_name
30
+
31
+ super method_name, include_all
32
+ end
33
+
34
+ private
35
+
36
+ def guard_test_hooks(method_name)
37
+ return unless [:after_load, :clear_hooks].include? method_name
38
+
39
+ raise ::Invar::ImmutableRealityError, ::Invar::ImmutableRealityError::HOOK_MSG
40
+ end
25
41
  end
26
42
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: invar
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robin Miller
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-13 00:00:00.000000000 Z
11
+ date: 2023-09-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dry-schema
@@ -51,6 +51,7 @@ files:
51
51
  - ".rspec"
52
52
  - ".rubocop.yml"
53
53
  - ".ruby-version"
54
+ - ".simplecov"
54
55
  - CODE_OF_CONDUCT.md
55
56
  - Gemfile
56
57
  - LICENSE.txt
@@ -62,6 +63,10 @@ files:
62
63
  - lib/invar/errors.rb
63
64
  - lib/invar/file_locator.rb
64
65
  - lib/invar/private_file.rb
66
+ - lib/invar/rake/task/config.rb
67
+ - lib/invar/rake/task/namespaced.rb
68
+ - lib/invar/rake/task/secrets.rb
69
+ - lib/invar/rake/task/status.rb
65
70
  - lib/invar/rake/tasks.rb
66
71
  - lib/invar/reality.rb
67
72
  - lib/invar/scope.rb