invar 0.9.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: c7bb218e77d7ce1acc82bf0160cc7386529f7e32e5a56cd2eea608bd2a191ad6
4
- data.tar.gz: 6b209fa6041be75736d28783d996c8fe0ee1e529bdde7d94dcb8e1bbda23423d
3
+ metadata.gz: 2346259fc26689ffca5386f39e982de60dc9f2d9c03399f85023e05a47802f7a
4
+ data.tar.gz: b6e2cea0c52fd0b52eb833036844c03d3a714d83e0572dc5dcbb4ae083c61cf7
5
5
  SHA512:
6
- metadata.gz: da15b60c94678b1dc9a5e002b3f4bba02d7b2e1a77923483a2a5427839b0cff0fd0e137ce9e1ca847e6b493bb134be97f419a20bef7903adabe8de8b0f163ae9
7
- data.tar.gz: 2d60ab3a7f5374478fe2ab872083ef4159620097e0cb65575eb6e6956b669c9fceaa86a39c085c637df125cebfc33f9ba52170a1a2389dce713de9d77da09c9d
6
+ metadata.gz: 1a9556b30d2c17fda9373551ecf6fffeaacabd35aa0e40bb883c78e8bd94642bf4bebbeea054b5f9a9681b0bb23b6ee8c30860afb732f94b9d7887e62e0c0c47
7
+ data.tar.gz: a80954a8dbfc4c42d3f57b94cb2830a8e598eda85cd95f37ff431a1ab2b253fa64d4c65f9aacba8cfa718f5b5f1082b05f78c0d1f97d2ae7bc58b4392c4e19f0
data/RELEASE_NOTES.md CHANGED
@@ -19,6 +19,21 @@ 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
+
22
37
  ## [0.9.0] - 2023-09-24
23
38
 
24
39
  ### Major Changes
@@ -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)
@@ -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::ConfigFileHandler.new(app_namespace)
66
- secrets = ::Invar::Rake::Tasks::SecretsFileHandler.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'
@@ -84,7 +79,7 @@ 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::ConfigFileHandler.new(app_namespace).edit
82
+ ::Invar::Rake::Task::ConfigFileHandler.new(app_namespace).edit
88
83
  end
89
84
 
90
85
  # alias
@@ -94,7 +89,7 @@ module Invar
94
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::SecretsFileHandler.new(app_namespace).edit
92
+ ::Invar::Rake::Task::SecretsFileHandler.new(app_namespace).edit
98
93
  end
99
94
 
100
95
  # alias
@@ -102,14 +97,14 @@ module Invar
102
97
 
103
98
  desc 'Encrypt the secrets file with a new generated key'
104
99
  task :rotate do
105
- ::Invar::Rake::Tasks::SecretsFileHandler.new(app_namespace).rotate
100
+ ::Invar::Rake::Task::SecretsFileHandler.new(app_namespace).rotate
106
101
  end
107
102
  end
108
103
 
109
104
  def define_info_tasks(app_namespace)
110
105
  desc 'Show directories to be searched for the given namespace'
111
106
  task :paths do
112
- ::Invar::Rake::Tasks::StatusHandler.new(app_namespace).show_paths
107
+ ::Invar::Rake::Task::StatusHandler.new(app_namespace).show_paths
113
108
  end
114
109
  end
115
110
 
@@ -136,194 +131,18 @@ module Invar
136
131
  warn msg
137
132
  exit 1
138
133
  end
134
+ end
139
135
 
140
- # Tasks that use a namespace for file searching
141
- class NamespacedFileTask
142
- def initialize(namespace)
143
- @locator = FileLocator.new(namespace)
144
- end
145
-
146
- def file_path
147
- config_dir / filename
148
- end
149
-
150
- private
151
-
152
- def config_dir
153
- @locator.search_paths.first
154
- end
155
- end
156
-
157
- # Configuration file actions.
158
- class ConfigFileHandler < NamespacedFileTask
159
- # Creates a config file in the appropriate location
160
- def create
161
- config_dir.mkpath
162
- file_path.write CONFIG_TEMPLATE
163
- file_path.chmod 0o600
164
-
165
- warn "Created file: #{ file_path }"
166
- end
167
-
168
- # Edits the existing config file in the appropriate location
169
- def edit
170
- content = $stdin.tty? ? nil : $stdin.read
171
- file_path = configs_file
172
-
173
- if content
174
- file_path.write content
175
- else
176
- system ENV.fetch('EDITOR', 'editor'), file_path.to_s, exception: true
177
- end
178
-
179
- warn "File saved to: #{ file_path }"
180
- end
181
-
182
- private
183
-
184
- def configs_file
185
- @locator.find 'config.yml'
186
- rescue ::Invar::FileLocator::FileNotFoundError => e
187
- warn <<~ERR
188
- Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
189
- #{ CREATE_SUGGESTION }
190
- ERR
191
- exit 1
192
- end
193
-
194
- def filename
195
- 'config.yml'
196
- end
197
- end
198
-
199
- # Secrets file actions.
200
- class SecretsFileHandler < NamespacedFileTask
201
- # Instructions hint for how to handle secret keys.
202
- SECRETS_INSTRUCTIONS = <<~INST
203
- Generated key. Save this key to a secure password manager, you will need it to edit the secrets.yml file:
204
- INST
205
-
206
- SWAP_EXT = 'tmp'
207
-
208
- # Creates a new encrypted secrets file and prints the generated encryption key to STDOUT
209
- def create(content: SECRETS_TEMPLATE)
210
- encryption_key = Lockbox.generate_key
211
-
212
- write_encrypted_file(file_path,
213
- encryption_key: encryption_key,
214
- content: content,
215
- permissions: PrivateFile::DEFAULT_PERMISSIONS)
216
-
217
- warn SECRETS_INSTRUCTIONS
218
- puts encryption_key
219
- end
220
-
221
- # Updates the file with new content.
222
- #
223
- # Either the content is provided over STDIN or the default editor is opened with the decrypted contents of
224
- # the secrets file. After closing the editor, the file will be updated with the new encrypted contents.
225
- def edit
226
- content = $stdin.tty? ? nil : $stdin.read
227
-
228
- edit_encrypted_file(secrets_file, content: content)
229
-
230
- warn "File saved to #{ secrets_file }"
231
- end
232
-
233
- def rotate
234
- file_path = secrets_file
235
-
236
- decrypted = read_encrypted_file(file_path, encryption_key: determine_key(file_path))
237
-
238
- swap_file = file_path.dirname / [file_path.basename, SWAP_EXT].join('.')
239
- file_path.rename swap_file
240
-
241
- begin
242
- create content: decrypted
243
- swap_file.delete
244
- rescue StandardError
245
- swap_file.rename file_path.to_s
246
- end
247
- end
248
-
249
- private
250
-
251
- def secrets_file
252
- @locator.find 'secrets.yml'
253
- rescue ::Invar::FileLocator::FileNotFoundError => e
254
- warn <<~ERR
255
- Abort: #{ e.message }. Searched in: #{ @locator.search_paths.join(', ') }
256
- #{ CREATE_SUGGESTION }
257
- ERR
258
- exit 1
259
- end
260
-
261
- def filename
262
- 'secrets.yml'
263
- end
264
-
265
- def read_encrypted_file(file_path, encryption_key:)
266
- lockbox = build_lockbox(encryption_key)
267
- lockbox.decrypt(file_path.binread)
268
- end
269
-
270
- def write_encrypted_file(file_path, encryption_key:, content:, permissions: nil)
271
- lockbox = build_lockbox(encryption_key)
272
-
273
- encrypted_data = lockbox.encrypt(content)
274
-
275
- config_dir.mkpath
276
- # TODO: replace File.opens with photo_path.binwrite(uri.data) once FakeFS can handle it
277
- File.open(file_path.to_s, 'wb') { |f| f.write encrypted_data }
278
- file_path.chmod permissions if permissions
279
-
280
- warn "Saved file: #{ file_path }"
281
- end
282
-
283
- def edit_encrypted_file(file_path, content: nil)
284
- encryption_key = determine_key(file_path)
285
-
286
- content ||= invoke_editor(file_path, encryption_key: encryption_key)
287
-
288
- write_encrypted_file(file_path, encryption_key: encryption_key, content: content)
289
- end
290
-
291
- def invoke_editor(file_path, encryption_key:)
292
- Tempfile.create(file_path.basename.to_s) do |tmp_file|
293
- decrypted = read_encrypted_file(file_path, encryption_key: encryption_key)
294
-
295
- tmp_file.write(decrypted)
296
- tmp_file.rewind # rewind needed because file does not get closed after write
297
- system(ENV.fetch('EDITOR', 'editor'), tmp_file.path, exception: true)
298
- tmp_file.read
299
- end
300
- end
301
-
302
- def determine_key(file_path)
303
- encryption_key = Lockbox.master_key
304
-
305
- if encryption_key.nil? && $stdin.respond_to?(:noecho)
306
- warn "Enter master key to decrypt #{ file_path }:"
307
- encryption_key = $stdin.noecho(&:gets).strip
308
- end
309
-
310
- encryption_key
311
- end
312
-
313
- def build_lockbox(encryption_key)
314
- Lockbox.new(key: encryption_key)
315
- rescue ArgumentError => e
316
- raise SecretsFileEncryptionError, e
317
- end
318
- 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
319
142
 
320
- # General status tasks
321
- class StatusHandler < NamespacedFileTask
322
- # Prints the current paths to be searched in
323
- def show_paths
324
- warn @locator.search_paths.join("\n")
325
- end
326
- 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
327
146
  end
328
147
  end
329
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.9.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.9.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-25 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
@@ -63,6 +63,10 @@ files:
63
63
  - lib/invar/errors.rb
64
64
  - lib/invar/file_locator.rb
65
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
66
70
  - lib/invar/rake/tasks.rb
67
71
  - lib/invar/reality.rb
68
72
  - lib/invar/scope.rb