pdk 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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