invar 0.8.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
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