pdk 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (153) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1329 -1321
  3. data/LICENSE +201 -201
  4. data/README.md +163 -163
  5. data/exe/pdk +10 -10
  6. data/lib/pdk/analytics/client/google_analytics.rb +143 -143
  7. data/lib/pdk/analytics/client/noop.rb +25 -25
  8. data/lib/pdk/analytics/util.rb +19 -19
  9. data/lib/pdk/analytics.rb +30 -30
  10. data/lib/pdk/answer_file.rb +12 -12
  11. data/lib/pdk/bolt.rb +19 -19
  12. data/lib/pdk/cli/build.rb +82 -82
  13. data/lib/pdk/cli/bundle.rb +48 -48
  14. data/lib/pdk/cli/config/get.rb +26 -26
  15. data/lib/pdk/cli/config.rb +22 -22
  16. data/lib/pdk/cli/console.rb +148 -148
  17. data/lib/pdk/cli/convert.rb +52 -52
  18. data/lib/pdk/cli/env.rb +52 -52
  19. data/lib/pdk/cli/errors.rb +25 -25
  20. data/lib/pdk/cli/exec/command.rb +293 -293
  21. data/lib/pdk/cli/exec/interactive_command.rb +114 -114
  22. data/lib/pdk/cli/exec.rb +84 -84
  23. data/lib/pdk/cli/exec_group.rb +104 -104
  24. data/lib/pdk/cli/get/config.rb +24 -24
  25. data/lib/pdk/cli/get.rb +20 -20
  26. data/lib/pdk/cli/module/build.rb +12 -12
  27. data/lib/pdk/cli/module/generate.rb +47 -47
  28. data/lib/pdk/cli/module.rb +14 -14
  29. data/lib/pdk/cli/new/class.rb +32 -32
  30. data/lib/pdk/cli/new/defined_type.rb +32 -32
  31. data/lib/pdk/cli/new/fact.rb +29 -29
  32. data/lib/pdk/cli/new/function.rb +29 -29
  33. data/lib/pdk/cli/new/module.rb +53 -53
  34. data/lib/pdk/cli/new/provider.rb +29 -29
  35. data/lib/pdk/cli/new/task.rb +34 -34
  36. data/lib/pdk/cli/new/test.rb +52 -52
  37. data/lib/pdk/cli/new/transport.rb +27 -27
  38. data/lib/pdk/cli/new.rb +21 -21
  39. data/lib/pdk/cli/release/prep.rb +39 -39
  40. data/lib/pdk/cli/release/publish.rb +50 -50
  41. data/lib/pdk/cli/release.rb +194 -194
  42. data/lib/pdk/cli/remove/config.rb +80 -80
  43. data/lib/pdk/cli/remove.rb +20 -20
  44. data/lib/pdk/cli/set/config.rb +119 -119
  45. data/lib/pdk/cli/set.rb +20 -20
  46. data/lib/pdk/cli/test/unit.rb +90 -90
  47. data/lib/pdk/cli/test.rb +11 -11
  48. data/lib/pdk/cli/update.rb +64 -64
  49. data/lib/pdk/cli/util/command_redirector.rb +27 -27
  50. data/lib/pdk/cli/util/interview.rb +72 -72
  51. data/lib/pdk/cli/util/option_normalizer.rb +55 -55
  52. data/lib/pdk/cli/util/option_validator.rb +68 -68
  53. data/lib/pdk/cli/util/spinner.rb +13 -13
  54. data/lib/pdk/cli/util/update_manager_printer.rb +82 -82
  55. data/lib/pdk/cli/util.rb +305 -305
  56. data/lib/pdk/cli/validate.rb +116 -116
  57. data/lib/pdk/cli.rb +175 -175
  58. data/lib/pdk/config/analytics_schema.json +26 -26
  59. data/lib/pdk/config/errors.rb +5 -5
  60. data/lib/pdk/config/ini_file.rb +183 -183
  61. data/lib/pdk/config/ini_file_setting.rb +39 -39
  62. data/lib/pdk/config/json.rb +34 -34
  63. data/lib/pdk/config/json_schema_namespace.rb +142 -142
  64. data/lib/pdk/config/json_schema_setting.rb +53 -53
  65. data/lib/pdk/config/json_with_schema.rb +49 -49
  66. data/lib/pdk/config/namespace.rb +354 -354
  67. data/lib/pdk/config/setting.rb +135 -135
  68. data/lib/pdk/config/validator.rb +31 -31
  69. data/lib/pdk/config/yaml.rb +46 -46
  70. data/lib/pdk/config/yaml_with_schema.rb +59 -59
  71. data/lib/pdk/config.rb +390 -390
  72. data/lib/pdk/context/control_repo.rb +60 -60
  73. data/lib/pdk/context/module.rb +28 -28
  74. data/lib/pdk/context/none.rb +22 -22
  75. data/lib/pdk/context.rb +99 -99
  76. data/lib/pdk/control_repo.rb +90 -90
  77. data/lib/pdk/generate/defined_type.rb +43 -43
  78. data/lib/pdk/generate/fact.rb +25 -25
  79. data/lib/pdk/generate/function.rb +48 -48
  80. data/lib/pdk/generate/module.rb +352 -352
  81. data/lib/pdk/generate/provider.rb +28 -28
  82. data/lib/pdk/generate/puppet_class.rb +43 -43
  83. data/lib/pdk/generate/puppet_object.rb +232 -232
  84. data/lib/pdk/generate/task.rb +68 -68
  85. data/lib/pdk/generate/transport.rb +33 -33
  86. data/lib/pdk/generate.rb +24 -24
  87. data/lib/pdk/i18n.rb +4 -4
  88. data/lib/pdk/logger.rb +45 -45
  89. data/lib/pdk/module/build.rb +322 -322
  90. data/lib/pdk/module/convert.rb +296 -296
  91. data/lib/pdk/module/metadata.rb +202 -202
  92. data/lib/pdk/module/release.rb +260 -260
  93. data/lib/pdk/module/update.rb +131 -131
  94. data/lib/pdk/module/update_manager.rb +227 -227
  95. data/lib/pdk/module.rb +30 -30
  96. data/lib/pdk/report/event.rb +370 -370
  97. data/lib/pdk/report.rb +121 -121
  98. data/lib/pdk/template/fetcher/git.rb +85 -85
  99. data/lib/pdk/template/fetcher/local.rb +28 -28
  100. data/lib/pdk/template/fetcher.rb +98 -98
  101. data/lib/pdk/template/renderer/v1/legacy_template_dir.rb +116 -116
  102. data/lib/pdk/template/renderer/v1/renderer.rb +132 -132
  103. data/lib/pdk/template/renderer/v1/template_file.rb +102 -102
  104. data/lib/pdk/template/renderer/v1.rb +25 -25
  105. data/lib/pdk/template/renderer.rb +96 -96
  106. data/lib/pdk/template/template_dir.rb +67 -67
  107. data/lib/pdk/template.rb +59 -59
  108. data/lib/pdk/tests/unit.rb +252 -252
  109. data/lib/pdk/util/bundler.rb +259 -259
  110. data/lib/pdk/util/changelog_generator.rb +137 -137
  111. data/lib/pdk/util/env.rb +47 -47
  112. data/lib/pdk/util/filesystem.rb +138 -138
  113. data/lib/pdk/util/git.rb +179 -179
  114. data/lib/pdk/util/json_finder.rb +85 -85
  115. data/lib/pdk/util/puppet_strings.rb +125 -125
  116. data/lib/pdk/util/puppet_version.rb +266 -266
  117. data/lib/pdk/util/ruby_version.rb +179 -179
  118. data/lib/pdk/util/template_uri.rb +295 -295
  119. data/lib/pdk/util/vendored_file.rb +93 -93
  120. data/lib/pdk/util/version.rb +43 -43
  121. data/lib/pdk/util/windows/api_types.rb +82 -82
  122. data/lib/pdk/util/windows/file.rb +36 -36
  123. data/lib/pdk/util/windows/process.rb +79 -79
  124. data/lib/pdk/util/windows/string.rb +16 -16
  125. data/lib/pdk/util/windows.rb +15 -15
  126. data/lib/pdk/util.rb +278 -277
  127. data/lib/pdk/validate/control_repo/control_repo_validator_group.rb +23 -23
  128. data/lib/pdk/validate/control_repo/environment_conf_validator.rb +98 -98
  129. data/lib/pdk/validate/external_command_validator.rb +208 -208
  130. data/lib/pdk/validate/internal_ruby_validator.rb +100 -100
  131. data/lib/pdk/validate/invokable_validator.rb +228 -228
  132. data/lib/pdk/validate/metadata/metadata_json_lint_validator.rb +86 -86
  133. data/lib/pdk/validate/metadata/metadata_syntax_validator.rb +78 -78
  134. data/lib/pdk/validate/metadata/metadata_validator_group.rb +20 -20
  135. data/lib/pdk/validate/puppet/puppet_epp_validator.rb +133 -133
  136. data/lib/pdk/validate/puppet/puppet_lint_validator.rb +66 -66
  137. data/lib/pdk/validate/puppet/puppet_syntax_validator.rb +137 -137
  138. data/lib/pdk/validate/puppet/puppet_validator_group.rb +21 -21
  139. data/lib/pdk/validate/ruby/ruby_rubocop_validator.rb +80 -80
  140. data/lib/pdk/validate/ruby/ruby_validator_group.rb +19 -19
  141. data/lib/pdk/validate/tasks/tasks_metadata_lint_validator.rb +88 -88
  142. data/lib/pdk/validate/tasks/tasks_name_validator.rb +50 -50
  143. data/lib/pdk/validate/tasks/tasks_validator_group.rb +20 -20
  144. data/lib/pdk/validate/validator.rb +118 -118
  145. data/lib/pdk/validate/validator_group.rb +104 -104
  146. data/lib/pdk/validate/yaml/yaml_syntax_validator.rb +95 -95
  147. data/lib/pdk/validate/yaml/yaml_validator_group.rb +19 -19
  148. data/lib/pdk/validate.rb +94 -94
  149. data/lib/pdk/version.rb +4 -4
  150. data/lib/pdk.rb +76 -76
  151. data/locales/config.yaml +21 -21
  152. data/locales/pdk.pot +2094 -2094
  153. metadata +5 -6
@@ -1,354 +1,354 @@
1
- require 'pdk'
2
-
3
- module PDK
4
- class Config
5
- class Namespace
6
- # @param value [String] the new name of this namespace.
7
- attr_writer :name
8
-
9
- # @return [String] the path to the file associated with the contents of
10
- # this namespace.
11
- attr_reader :file
12
-
13
- # @return [self] the parent namespace of this namespace.
14
- attr_accessor :parent
15
-
16
- # Initialises the PDK::Config::Namespace object.
17
- #
18
- # @param name [String] the name of the namespace (defaults to nil).
19
- # @param params [Hash{Symbol => Object}] keyword parameters for the
20
- # method.
21
- # @option params [String] :file the path to the file associated with the
22
- # contents of the namespace (defaults to nil).
23
- # @option params [self] :parent the parent {self} that this namespace is
24
- # a child of (defaults to nil).
25
- # @option params [self] :persistent_defaults whether default values should be persisted
26
- # to disk when evaluated. By default they are not persisted to disk. This is typically
27
- # used for settings which a randomly generated, instead of being deterministic, e.g. analytics user-id
28
- # @param block [Proc] a block that is evaluated within the new instance.
29
- def initialize(name = nil, file: nil, parent: nil, persistent_defaults: false, &block)
30
- @file = PDK::Util::Filesystem.expand_path(file) unless file.nil?
31
- @settings = {}
32
- @name = name.to_s
33
- @parent = parent
34
- @persistent_defaults = persistent_defaults
35
- @mounts = {}
36
- @loaded_from_file = false
37
- @read_only = false
38
-
39
- instance_eval(&block) if block_given?
40
- end
41
-
42
- # Pre-configure a value in the namespace.
43
- #
44
- # Allows you to specify validators and a default value for value in the
45
- # namespace (see PDK::Config::Value#initialize).
46
- #
47
- # @param key [String,Symbol] the name of the value.
48
- # @param block [Proc] a block that is evaluated within the new [self].
49
- #
50
- # @return [nil]
51
- def setting(key, &block)
52
- @settings[key.to_s] ||= default_setting_class.new(key.to_s, self)
53
- @settings[key.to_s].instance_eval(&block) if block_given?
54
- end
55
-
56
- # Mount a provided [self] (or subclass) into the namespace.
57
- #
58
- # @param key [String,Symbol] the name of the namespace to be mounted.
59
- # @param obj [self] the namespace to be mounted.
60
- # @param block [Proc] a block to be evaluated within the instance of the
61
- # newly mounted namespace.
62
- #
63
- # @raise [ArgumentError] if the object to be mounted is not a {self} or
64
- # subclass thereof.
65
- #
66
- # @return [self] the mounted namespace.
67
- def mount(key, obj, &block)
68
- raise ArgumentError, _('Only PDK::Config::Namespace objects can be mounted into a namespace') unless obj.is_a?(PDK::Config::Namespace)
69
- obj.parent = self
70
- obj.name = key.to_s
71
- obj.instance_eval(&block) if block_given?
72
- @mounts[key.to_s] = obj
73
- end
74
-
75
- # Create and mount a new child namespace.
76
- #
77
- # @param name [String,Symbol] the name of the new namespace.
78
- # @param block [Proc]
79
- def namespace(name, &block)
80
- mount(name, PDK::Config::Namespace.new, &block)
81
- end
82
-
83
- # Get the value of the named key.
84
- #
85
- # If there is a value for that key, return it. If not, follow the logic
86
- # described in {#default_config_value} to determine the default value to
87
- # return.
88
- #
89
- # @note Unlike a Ruby Hash, this will not return `nil` in the event that
90
- # the key does not exist (see #fetch).
91
- #
92
- # @param key [String,Symbol] the name of the value to retrieve.
93
- #
94
- # @return [Object] the requested value.
95
- def [](key)
96
- # Check if it's a mount first...
97
- return @mounts[key.to_s] unless @mounts[key.to_s].nil?
98
- # Check if it's a setting, otherwise nil
99
- return nil if settings[key.to_s].nil?
100
- return settings[key.to_s].value unless settings[key.to_s].value.nil?
101
- # Duplicate arrays and hashes so that they are isolated from changes being made
102
- default_value = PDK::Util.deep_duplicate(settings[key.to_s].default)
103
- return default_value if default_value.nil? || !@persistent_defaults
104
- # Persist the default value
105
- settings[key.to_s].value = default_value
106
- save_data
107
- default_value
108
- end
109
-
110
- # Get the value of the named key or the provided default value if not
111
- # present. Note that this does not trigger persistent defaults
112
- #
113
- # This differs from {#[]} in an important way in that it allows you to
114
- # return a default value, which is not possible using `[] || default` as
115
- # non-existent values when accessed normally via {#[]} will be defaulted
116
- # to a new Hash.
117
- #
118
- # @param key [String,Symbol] the name of the value to fetch.
119
- # @param default_value [Object] the value to return if the namespace does
120
- # not contain the requested value.
121
- #
122
- # @return [Object] the requested value.
123
- def fetch(key, default_value)
124
- # Check if it's a mount first...
125
- return @mounts[key.to_s] unless @mounts[key.to_s].nil?
126
- # Check if it's a setting, otherwise default_value
127
- return default_value if settings[key.to_s].nil?
128
- # Check if has a value, otherwise default_value
129
- settings[key.to_s].value.nil? ? default_value : settings[key.to_s].value
130
- end
131
-
132
- # After the value has been set in memory, the value will then be
133
- # persisted to disk.
134
- #
135
- # @param key [String,Symbol] the name of the configuration value.
136
- # @param value [Object] the value of the configuration value.
137
- #
138
- # @return [nil]
139
- def []=(key, value)
140
- # You can't set the value of a mount
141
- raise ArgumentError, _('Namespace mounts can not be set a value') unless @mounts[key.to_s].nil?
142
- set_volatile_value(key, value)
143
- # Persist the change
144
- save_data
145
- end
146
-
147
- # Convert the namespace into a Hash of values, suitable for serialising
148
- # and persisting to disk.
149
- #
150
- # Child namespaces that are associated with their own files are excluded
151
- # from the Hash (as their values will be persisted to their own files)
152
- # and nil values are removed from the Hash.
153
- #
154
- # @return [Hash{String => Object}] the values from the namespace that
155
- # should be persisted to disk.
156
- def to_h
157
- new_hash = {}
158
- settings.each_pair { |k, v| new_hash[k] = v.value }
159
- @mounts.each_pair { |k, mount_point| new_hash[k] = mount_point.to_h if mount_point.include_in_parent? }
160
- new_hash.delete_if { |_, v| v.nil? }
161
- new_hash
162
- end
163
-
164
- # Resolves all filtered settings, including child namespaces, fully namespaced and filling in default values.
165
- #
166
- # @param filter [String] Only resolve setting names which match the filter. See #be_resolved? for matching rules
167
- # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'}
168
- def resolve(filter = nil)
169
- resolved = {}
170
- # Resolve the settings
171
- settings.values.each do |setting|
172
- setting_name = setting.qualified_name
173
- if be_resolved?(setting_name, filter)
174
- resolved[setting_name] = setting.value.nil? ? setting.default : setting.value
175
- end
176
- end
177
- # Resolve the mounts
178
- @mounts.values.each { |mount| resolved.merge!(mount.resolve(filter)) }
179
- resolved
180
- end
181
-
182
- # @return [Boolean] true if the namespace has a parent, otherwise false.
183
- def child_namespace?
184
- !parent.nil?
185
- end
186
-
187
- # Determines the fully qualified name of the namespace.
188
- #
189
- # If this is a child namespace, then fully qualified name for the
190
- # namespace will be "<parent>.<child>".
191
- #
192
- # @return [String] the fully qualifed name of the namespace.
193
- def name
194
- child_namespace? ? [parent.name, @name].join('.') : @name
195
- end
196
-
197
- # Determines if the contents of the namespace should be included in the
198
- # parent namespace when persisting to disk.
199
- #
200
- # If the namespace has been mounted into a parent namespace and is not
201
- # associated with its own file on disk, then the values in the namespace
202
- # should be included in the parent namespace when persisting to disk.
203
- #
204
- # @return [Boolean] true if the values should be included in the parent
205
- # namespace.
206
- def include_in_parent?
207
- child_namespace? && file.nil?
208
- end
209
-
210
- # Disables the namespace, and child namespaces, from writing changes to disk.
211
- # Typically this is only needed for unit testing.
212
- # @api private
213
- def read_only!
214
- @read_only = true
215
- @mounts.each { |_, child| child.read_only! }
216
- end
217
-
218
- private
219
-
220
- # Returns the object class to create settings with. Subclasses may override this to use specific setting classes
221
- #
222
- # @return [Class[PDK::Config::Setting]]
223
- #
224
- # @abstract
225
- # @private
226
- def default_setting_class
227
- PDK::Config::Setting
228
- end
229
-
230
- # Determines whether a setting name should be resolved using the filter
231
- # Returns true when filter is nil.
232
- # Returns true if the filter is exactly the same name as the setting.
233
- # Returns true if the name is a sub-key of the filter e.g.
234
- # Given a filter of user.module_defaults, `user.module_defaults.author` will return true, but `user.analytics.disabled` will return false.
235
- #
236
- # @param name [String] The setting name to test.
237
- # @param filter [String] The filter used to test on the name.
238
- # @return [Boolean] Whether the name passes the filter.
239
- def be_resolved?(name, filter = nil)
240
- return true if filter.nil? # If we're not filtering, this value should always be resolved
241
- return true if name == filter # If it's exactly the same name then it should be resolved
242
- name.start_with?(filter + '.') # If name is a subkey of the filter then it should be resolved
243
- end
244
-
245
- # @abstract Subclass and override {#parse_file} to implement parsing logic
246
- # for a particular config file format.
247
- #
248
- # @param data [String] The content of the file to be parsed.
249
- # @param filename [String] The path to the file to be parsed.
250
- #
251
- # @yield [String, Object] the data to be loaded into the
252
- # namespace.
253
- def parse_file(_filename); end
254
-
255
- # @abstract Subclass and override {#serialize_data} to implement generating
256
- # logic for a particular config file format.
257
- #
258
- # @param data [Hash{String => Object}] the data stored in the namespace
259
- #
260
- # @return [String] the serialized contents of the namespace suitable for
261
- # writing to disk.
262
- def serialize_data(_data); end
263
-
264
- # @abstract Subclass and override {#create_missing_setting} to implement logic
265
- # when a setting is dynamically created, for example when attempting to
266
- # set the value of an unknown setting
267
- #
268
- # @param data [Hash{String => Object}] the data stored in the namespace
269
- #
270
- # @return [String] the serialized contents of the namespace suitable for
271
- # writing to disk.
272
- def create_missing_setting(key, initial_value = nil)
273
- # Need to use `@settings` and `@mounts` here to stop recursive calls
274
- return unless @mounts[key.to_s].nil?
275
- return unless @settings[key.to_s].nil?
276
- @settings[key.to_s] = default_setting_class.new(key.to_s, self, initial_value)
277
- end
278
-
279
- # Set the value of the named key.
280
- #
281
- # If the key has been pre-configured with {#value}, then the value of the
282
- # key will be validated against any validators that have been configured.
283
- #
284
- # @param key [String,Symbol] the name of the configuration value.
285
- # @param value [Object] the value of the configuration value.
286
- def set_volatile_value(key, value)
287
- # Need to use `settings` here to force the backing file to be loaded
288
- return create_missing_setting(key, value) if settings[key.to_s].nil?
289
- # Need to use `@settings` here to stop recursive calls from []=
290
- @settings[key.to_s].value = value
291
- end
292
-
293
- # Helper method to read files.
294
- #
295
- # @raise [PDK::Config::LoadError] if the file is removed during read.
296
- # @raise [PDK::Config::LoadError] if the user doesn't have the
297
- # permissions needed to read the file.
298
- # @return [String,nil] the contents of the file or nil if the file does
299
- # not exist.
300
- def load_data(filename)
301
- return if filename.nil?
302
- return unless PDK::Util::Filesystem.file?(filename)
303
-
304
- PDK::Util::Filesystem.read_file(filename)
305
- rescue Errno::ENOENT => e
306
- raise PDK::Config::LoadError, e.message
307
- rescue Errno::EACCES
308
- raise PDK::Config::LoadError, _('Unable to open %{file} for reading') % {
309
- file: filename,
310
- }
311
- end
312
-
313
- # Persist the contents of the namespace to disk.
314
- #
315
- # Directories will be automatically created and the contents of the
316
- # namespace will be serialized automatically with {#serialize_data}.
317
- #
318
- # @raise [PDK::Config::LoadError] if one of the intermediary path components
319
- # exist but is not a directory.
320
- # @raise [PDK::Config::LoadError] if the user does not have the
321
- # permissions needed to write the file.
322
- #
323
- # @return [nil]
324
- def save_data
325
- return if file.nil? || @read_only
326
-
327
- PDK::Util::Filesystem.mkdir_p(File.dirname(file))
328
-
329
- PDK::Util::Filesystem.write_file(file, serialize_data(to_h))
330
- rescue Errno::EACCES
331
- raise PDK::Config::LoadError, _('Unable to open %{file} for writing') % {
332
- file: file,
333
- }
334
- rescue SystemCallError => e
335
- raise PDK::Config::LoadError, e.message
336
- end
337
-
338
- # Memoised accessor for the loaded data.
339
- #
340
- # @return [Hash<String => PDK::Config::Setting>] the contents of the namespace.
341
- def settings
342
- return @settings if @loaded_from_file
343
- @loaded_from_file = true
344
- return @settings if file.nil?
345
- parse_file(file) do |key, parsed_setting|
346
- # Create a settings chain if a setting already exists
347
- parsed_setting.previous_setting = @settings[key] unless @settings[key].nil?
348
- @settings[key] = parsed_setting
349
- end
350
- @settings
351
- end
352
- end
353
- end
354
- end
1
+ require 'pdk'
2
+
3
+ module PDK
4
+ class Config
5
+ class Namespace
6
+ # @param value [String] the new name of this namespace.
7
+ attr_writer :name
8
+
9
+ # @return [String] the path to the file associated with the contents of
10
+ # this namespace.
11
+ attr_reader :file
12
+
13
+ # @return [self] the parent namespace of this namespace.
14
+ attr_accessor :parent
15
+
16
+ # Initialises the PDK::Config::Namespace object.
17
+ #
18
+ # @param name [String] the name of the namespace (defaults to nil).
19
+ # @param params [Hash{Symbol => Object}] keyword parameters for the
20
+ # method.
21
+ # @option params [String] :file the path to the file associated with the
22
+ # contents of the namespace (defaults to nil).
23
+ # @option params [self] :parent the parent {self} that this namespace is
24
+ # a child of (defaults to nil).
25
+ # @option params [self] :persistent_defaults whether default values should be persisted
26
+ # to disk when evaluated. By default they are not persisted to disk. This is typically
27
+ # used for settings which a randomly generated, instead of being deterministic, e.g. analytics user-id
28
+ # @param block [Proc] a block that is evaluated within the new instance.
29
+ def initialize(name = nil, file: nil, parent: nil, persistent_defaults: false, &block)
30
+ @file = PDK::Util::Filesystem.expand_path(file) unless file.nil?
31
+ @settings = {}
32
+ @name = name.to_s
33
+ @parent = parent
34
+ @persistent_defaults = persistent_defaults
35
+ @mounts = {}
36
+ @loaded_from_file = false
37
+ @read_only = false
38
+
39
+ instance_eval(&block) if block_given?
40
+ end
41
+
42
+ # Pre-configure a value in the namespace.
43
+ #
44
+ # Allows you to specify validators and a default value for value in the
45
+ # namespace (see PDK::Config::Value#initialize).
46
+ #
47
+ # @param key [String,Symbol] the name of the value.
48
+ # @param block [Proc] a block that is evaluated within the new [self].
49
+ #
50
+ # @return [nil]
51
+ def setting(key, &block)
52
+ @settings[key.to_s] ||= default_setting_class.new(key.to_s, self)
53
+ @settings[key.to_s].instance_eval(&block) if block_given?
54
+ end
55
+
56
+ # Mount a provided [self] (or subclass) into the namespace.
57
+ #
58
+ # @param key [String,Symbol] the name of the namespace to be mounted.
59
+ # @param obj [self] the namespace to be mounted.
60
+ # @param block [Proc] a block to be evaluated within the instance of the
61
+ # newly mounted namespace.
62
+ #
63
+ # @raise [ArgumentError] if the object to be mounted is not a {self} or
64
+ # subclass thereof.
65
+ #
66
+ # @return [self] the mounted namespace.
67
+ def mount(key, obj, &block)
68
+ raise ArgumentError, _('Only PDK::Config::Namespace objects can be mounted into a namespace') unless obj.is_a?(PDK::Config::Namespace)
69
+ obj.parent = self
70
+ obj.name = key.to_s
71
+ obj.instance_eval(&block) if block_given?
72
+ @mounts[key.to_s] = obj
73
+ end
74
+
75
+ # Create and mount a new child namespace.
76
+ #
77
+ # @param name [String,Symbol] the name of the new namespace.
78
+ # @param block [Proc]
79
+ def namespace(name, &block)
80
+ mount(name, PDK::Config::Namespace.new, &block)
81
+ end
82
+
83
+ # Get the value of the named key.
84
+ #
85
+ # If there is a value for that key, return it. If not, follow the logic
86
+ # described in {#default_config_value} to determine the default value to
87
+ # return.
88
+ #
89
+ # @note Unlike a Ruby Hash, this will not return `nil` in the event that
90
+ # the key does not exist (see #fetch).
91
+ #
92
+ # @param key [String,Symbol] the name of the value to retrieve.
93
+ #
94
+ # @return [Object] the requested value.
95
+ def [](key)
96
+ # Check if it's a mount first...
97
+ return @mounts[key.to_s] unless @mounts[key.to_s].nil?
98
+ # Check if it's a setting, otherwise nil
99
+ return nil if settings[key.to_s].nil?
100
+ return settings[key.to_s].value unless settings[key.to_s].value.nil?
101
+ # Duplicate arrays and hashes so that they are isolated from changes being made
102
+ default_value = PDK::Util.deep_duplicate(settings[key.to_s].default)
103
+ return default_value if default_value.nil? || !@persistent_defaults
104
+ # Persist the default value
105
+ settings[key.to_s].value = default_value
106
+ save_data
107
+ default_value
108
+ end
109
+
110
+ # Get the value of the named key or the provided default value if not
111
+ # present. Note that this does not trigger persistent defaults
112
+ #
113
+ # This differs from {#[]} in an important way in that it allows you to
114
+ # return a default value, which is not possible using `[] || default` as
115
+ # non-existent values when accessed normally via {#[]} will be defaulted
116
+ # to a new Hash.
117
+ #
118
+ # @param key [String,Symbol] the name of the value to fetch.
119
+ # @param default_value [Object] the value to return if the namespace does
120
+ # not contain the requested value.
121
+ #
122
+ # @return [Object] the requested value.
123
+ def fetch(key, default_value)
124
+ # Check if it's a mount first...
125
+ return @mounts[key.to_s] unless @mounts[key.to_s].nil?
126
+ # Check if it's a setting, otherwise default_value
127
+ return default_value if settings[key.to_s].nil?
128
+ # Check if has a value, otherwise default_value
129
+ settings[key.to_s].value.nil? ? default_value : settings[key.to_s].value
130
+ end
131
+
132
+ # After the value has been set in memory, the value will then be
133
+ # persisted to disk.
134
+ #
135
+ # @param key [String,Symbol] the name of the configuration value.
136
+ # @param value [Object] the value of the configuration value.
137
+ #
138
+ # @return [nil]
139
+ def []=(key, value)
140
+ # You can't set the value of a mount
141
+ raise ArgumentError, _('Namespace mounts can not be set a value') unless @mounts[key.to_s].nil?
142
+ set_volatile_value(key, value)
143
+ # Persist the change
144
+ save_data
145
+ end
146
+
147
+ # Convert the namespace into a Hash of values, suitable for serialising
148
+ # and persisting to disk.
149
+ #
150
+ # Child namespaces that are associated with their own files are excluded
151
+ # from the Hash (as their values will be persisted to their own files)
152
+ # and nil values are removed from the Hash.
153
+ #
154
+ # @return [Hash{String => Object}] the values from the namespace that
155
+ # should be persisted to disk.
156
+ def to_h
157
+ new_hash = {}
158
+ settings.each_pair { |k, v| new_hash[k] = v.value }
159
+ @mounts.each_pair { |k, mount_point| new_hash[k] = mount_point.to_h if mount_point.include_in_parent? }
160
+ new_hash.delete_if { |_, v| v.nil? }
161
+ new_hash
162
+ end
163
+
164
+ # Resolves all filtered settings, including child namespaces, fully namespaced and filling in default values.
165
+ #
166
+ # @param filter [String] Only resolve setting names which match the filter. See #be_resolved? for matching rules
167
+ # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'}
168
+ def resolve(filter = nil)
169
+ resolved = {}
170
+ # Resolve the settings
171
+ settings.values.each do |setting|
172
+ setting_name = setting.qualified_name
173
+ if be_resolved?(setting_name, filter)
174
+ resolved[setting_name] = setting.value.nil? ? setting.default : setting.value
175
+ end
176
+ end
177
+ # Resolve the mounts
178
+ @mounts.values.each { |mount| resolved.merge!(mount.resolve(filter)) }
179
+ resolved
180
+ end
181
+
182
+ # @return [Boolean] true if the namespace has a parent, otherwise false.
183
+ def child_namespace?
184
+ !parent.nil?
185
+ end
186
+
187
+ # Determines the fully qualified name of the namespace.
188
+ #
189
+ # If this is a child namespace, then fully qualified name for the
190
+ # namespace will be "<parent>.<child>".
191
+ #
192
+ # @return [String] the fully qualifed name of the namespace.
193
+ def name
194
+ child_namespace? ? [parent.name, @name].join('.') : @name
195
+ end
196
+
197
+ # Determines if the contents of the namespace should be included in the
198
+ # parent namespace when persisting to disk.
199
+ #
200
+ # If the namespace has been mounted into a parent namespace and is not
201
+ # associated with its own file on disk, then the values in the namespace
202
+ # should be included in the parent namespace when persisting to disk.
203
+ #
204
+ # @return [Boolean] true if the values should be included in the parent
205
+ # namespace.
206
+ def include_in_parent?
207
+ child_namespace? && file.nil?
208
+ end
209
+
210
+ # Disables the namespace, and child namespaces, from writing changes to disk.
211
+ # Typically this is only needed for unit testing.
212
+ # @api private
213
+ def read_only!
214
+ @read_only = true
215
+ @mounts.each { |_, child| child.read_only! }
216
+ end
217
+
218
+ private
219
+
220
+ # Returns the object class to create settings with. Subclasses may override this to use specific setting classes
221
+ #
222
+ # @return [Class[PDK::Config::Setting]]
223
+ #
224
+ # @abstract
225
+ # @private
226
+ def default_setting_class
227
+ PDK::Config::Setting
228
+ end
229
+
230
+ # Determines whether a setting name should be resolved using the filter
231
+ # Returns true when filter is nil.
232
+ # Returns true if the filter is exactly the same name as the setting.
233
+ # Returns true if the name is a sub-key of the filter e.g.
234
+ # Given a filter of user.module_defaults, `user.module_defaults.author` will return true, but `user.analytics.disabled` will return false.
235
+ #
236
+ # @param name [String] The setting name to test.
237
+ # @param filter [String] The filter used to test on the name.
238
+ # @return [Boolean] Whether the name passes the filter.
239
+ def be_resolved?(name, filter = nil)
240
+ return true if filter.nil? # If we're not filtering, this value should always be resolved
241
+ return true if name == filter # If it's exactly the same name then it should be resolved
242
+ name.start_with?(filter + '.') # If name is a subkey of the filter then it should be resolved
243
+ end
244
+
245
+ # @abstract Subclass and override {#parse_file} to implement parsing logic
246
+ # for a particular config file format.
247
+ #
248
+ # @param data [String] The content of the file to be parsed.
249
+ # @param filename [String] The path to the file to be parsed.
250
+ #
251
+ # @yield [String, Object] the data to be loaded into the
252
+ # namespace.
253
+ def parse_file(_filename); end
254
+
255
+ # @abstract Subclass and override {#serialize_data} to implement generating
256
+ # logic for a particular config file format.
257
+ #
258
+ # @param data [Hash{String => Object}] the data stored in the namespace
259
+ #
260
+ # @return [String] the serialized contents of the namespace suitable for
261
+ # writing to disk.
262
+ def serialize_data(_data); end
263
+
264
+ # @abstract Subclass and override {#create_missing_setting} to implement logic
265
+ # when a setting is dynamically created, for example when attempting to
266
+ # set the value of an unknown setting
267
+ #
268
+ # @param data [Hash{String => Object}] the data stored in the namespace
269
+ #
270
+ # @return [String] the serialized contents of the namespace suitable for
271
+ # writing to disk.
272
+ def create_missing_setting(key, initial_value = nil)
273
+ # Need to use `@settings` and `@mounts` here to stop recursive calls
274
+ return unless @mounts[key.to_s].nil?
275
+ return unless @settings[key.to_s].nil?
276
+ @settings[key.to_s] = default_setting_class.new(key.to_s, self, initial_value)
277
+ end
278
+
279
+ # Set the value of the named key.
280
+ #
281
+ # If the key has been pre-configured with {#value}, then the value of the
282
+ # key will be validated against any validators that have been configured.
283
+ #
284
+ # @param key [String,Symbol] the name of the configuration value.
285
+ # @param value [Object] the value of the configuration value.
286
+ def set_volatile_value(key, value)
287
+ # Need to use `settings` here to force the backing file to be loaded
288
+ return create_missing_setting(key, value) if settings[key.to_s].nil?
289
+ # Need to use `@settings` here to stop recursive calls from []=
290
+ @settings[key.to_s].value = value
291
+ end
292
+
293
+ # Helper method to read files.
294
+ #
295
+ # @raise [PDK::Config::LoadError] if the file is removed during read.
296
+ # @raise [PDK::Config::LoadError] if the user doesn't have the
297
+ # permissions needed to read the file.
298
+ # @return [String,nil] the contents of the file or nil if the file does
299
+ # not exist.
300
+ def load_data(filename)
301
+ return if filename.nil?
302
+ return unless PDK::Util::Filesystem.file?(filename)
303
+
304
+ PDK::Util::Filesystem.read_file(filename)
305
+ rescue Errno::ENOENT => e
306
+ raise PDK::Config::LoadError, e.message
307
+ rescue Errno::EACCES
308
+ raise PDK::Config::LoadError, _('Unable to open %{file} for reading') % {
309
+ file: filename,
310
+ }
311
+ end
312
+
313
+ # Persist the contents of the namespace to disk.
314
+ #
315
+ # Directories will be automatically created and the contents of the
316
+ # namespace will be serialized automatically with {#serialize_data}.
317
+ #
318
+ # @raise [PDK::Config::LoadError] if one of the intermediary path components
319
+ # exist but is not a directory.
320
+ # @raise [PDK::Config::LoadError] if the user does not have the
321
+ # permissions needed to write the file.
322
+ #
323
+ # @return [nil]
324
+ def save_data
325
+ return if file.nil? || @read_only
326
+
327
+ PDK::Util::Filesystem.mkdir_p(File.dirname(file))
328
+
329
+ PDK::Util::Filesystem.write_file(file, serialize_data(to_h))
330
+ rescue Errno::EACCES
331
+ raise PDK::Config::LoadError, _('Unable to open %{file} for writing') % {
332
+ file: file,
333
+ }
334
+ rescue SystemCallError => e
335
+ raise PDK::Config::LoadError, e.message
336
+ end
337
+
338
+ # Memoised accessor for the loaded data.
339
+ #
340
+ # @return [Hash<String => PDK::Config::Setting>] the contents of the namespace.
341
+ def settings
342
+ return @settings if @loaded_from_file
343
+ @loaded_from_file = true
344
+ return @settings if file.nil?
345
+ parse_file(file) do |key, parsed_setting|
346
+ # Create a settings chain if a setting already exists
347
+ parsed_setting.previous_setting = @settings[key] unless @settings[key].nil?
348
+ @settings[key] = parsed_setting
349
+ end
350
+ @settings
351
+ end
352
+ end
353
+ end
354
+ end