pdk 1.10.0 → 1.11.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 (57) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +50 -1
  3. data/lib/pdk.rb +16 -1
  4. data/lib/pdk/analytics.rb +28 -0
  5. data/lib/pdk/analytics/client/google_analytics.rb +138 -0
  6. data/lib/pdk/analytics/client/noop.rb +23 -0
  7. data/lib/pdk/analytics/util.rb +17 -0
  8. data/lib/pdk/cli.rb +37 -0
  9. data/lib/pdk/cli/build.rb +2 -0
  10. data/lib/pdk/cli/bundle.rb +2 -1
  11. data/lib/pdk/cli/convert.rb +2 -0
  12. data/lib/pdk/cli/exec.rb +28 -1
  13. data/lib/pdk/cli/new/class.rb +2 -0
  14. data/lib/pdk/cli/new/defined_type.rb +2 -0
  15. data/lib/pdk/cli/new/module.rb +2 -0
  16. data/lib/pdk/cli/new/provider.rb +2 -0
  17. data/lib/pdk/cli/new/task.rb +2 -0
  18. data/lib/pdk/cli/test.rb +0 -1
  19. data/lib/pdk/cli/test/unit.rb +13 -10
  20. data/lib/pdk/cli/update.rb +21 -0
  21. data/lib/pdk/cli/util.rb +35 -0
  22. data/lib/pdk/cli/util/interview.rb +7 -1
  23. data/lib/pdk/cli/validate.rb +9 -2
  24. data/lib/pdk/config.rb +94 -0
  25. data/lib/pdk/config/errors.rb +5 -0
  26. data/lib/pdk/config/json.rb +23 -0
  27. data/lib/pdk/config/namespace.rb +273 -0
  28. data/lib/pdk/config/validator.rb +31 -0
  29. data/lib/pdk/config/value.rb +94 -0
  30. data/lib/pdk/config/yaml.rb +31 -0
  31. data/lib/pdk/generate/module.rb +3 -2
  32. data/lib/pdk/logger.rb +21 -1
  33. data/lib/pdk/module/build.rb +58 -0
  34. data/lib/pdk/module/convert.rb +1 -1
  35. data/lib/pdk/module/metadata.rb +1 -0
  36. data/lib/pdk/module/templatedir.rb +24 -5
  37. data/lib/pdk/module/update_manager.rb +2 -2
  38. data/lib/pdk/report/event.rb +3 -3
  39. data/lib/pdk/template_file.rb +1 -1
  40. data/lib/pdk/tests/unit.rb +10 -12
  41. data/lib/pdk/util.rb +9 -0
  42. data/lib/pdk/util/bundler.rb +5 -9
  43. data/lib/pdk/util/filesystem.rb +37 -0
  44. data/lib/pdk/util/puppet_version.rb +1 -1
  45. data/lib/pdk/util/ruby_version.rb +16 -6
  46. data/lib/pdk/util/template_uri.rb +72 -43
  47. data/lib/pdk/util/version.rb +1 -1
  48. data/lib/pdk/util/windows.rb +1 -0
  49. data/lib/pdk/util/windows/api_types.rb +0 -7
  50. data/lib/pdk/util/windows/file.rb +1 -1
  51. data/lib/pdk/util/windows/string.rb +1 -1
  52. data/lib/pdk/validate/base_validator.rb +8 -6
  53. data/lib/pdk/validate/puppet/puppet_syntax.rb +1 -1
  54. data/lib/pdk/validate/ruby/rubocop.rb +1 -1
  55. data/lib/pdk/version.rb +1 -1
  56. data/locales/pdk.pot +223 -114
  57. metadata +103 -50
@@ -24,6 +24,8 @@ module PDK::CLI
24
24
  raise PDK::CLI::ExitWithError, _("'%{name}' is not a valid class name") % { name: class_name }
25
25
  end
26
26
 
27
+ PDK::CLI::Util.analytics_screen_view('new_class', opts)
28
+
27
29
  PDK::Generate::PuppetClass.new(module_dir, class_name, opts).run
28
30
  end
29
31
  end
@@ -22,6 +22,8 @@ module PDK::CLI
22
22
  raise PDK::CLI::ExitWithError, _("'%{name}' is not a valid defined type name") % { name: defined_type_name }
23
23
  end
24
24
 
25
+ PDK::CLI::Util.analytics_screen_view('new_defined_type', opts)
26
+
25
27
  PDK::Generate::DefinedType.new(module_dir, defined_type_name, opts).run
26
28
  end
27
29
  end
@@ -21,6 +21,8 @@ module PDK::CLI
21
21
 
22
22
  PDK::CLI::Util.validate_template_opts(opts)
23
23
 
24
+ PDK::CLI::Util.analytics_screen_view('new_module', opts)
25
+
24
26
  if opts[:'skip-interview'] && opts[:'full-interview']
25
27
  PDK.logger.info _('Ignoring --full-interview and continuing with --skip-interview.')
26
28
  opts[:'full-interview'] = false
@@ -19,6 +19,8 @@ module PDK::CLI
19
19
  raise PDK::CLI::ExitWithError, _("'%{name}' is not a valid provider name") % { name: provider_name }
20
20
  end
21
21
 
22
+ PDK::CLI::Util.analytics_screen_view('new_provider', opts)
23
+
22
24
  PDK::Generate::Provider.new(module_dir, provider_name, opts).run
23
25
  end
24
26
  end
@@ -24,6 +24,8 @@ module PDK::CLI
24
24
  raise PDK::CLI::ExitWithError, _("'%{name}' is not a valid task name") % { name: task_name }
25
25
  end
26
26
 
27
+ PDK::CLI::Util.analytics_screen_view('new_task', opts)
28
+
27
29
  PDK::Generate::Task.new(module_dir, task_name, opts).run
28
30
  end
29
31
  end
data/lib/pdk/cli/test.rb CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  module PDK::CLI
3
2
  @test_cmd = @base_cmd.define_command do
4
3
  name 'test'
@@ -33,10 +33,22 @@ module PDK::CLI
33
33
 
34
34
  PDK::CLI::Util.module_version_check
35
35
 
36
+ PDK::CLI::Util.analytics_screen_view('test_unit', opts)
37
+
38
+ # Ensure that the bundled gems are up to date and correct Ruby is activated before running or listing tests.
39
+ puppet_env = PDK::CLI::Util.puppet_from_opts_or_env(opts)
40
+ PDK::Util::PuppetVersion.fetch_puppet_dev if opts[:'puppet-dev']
41
+ PDK::Util::RubyVersion.use(puppet_env[:ruby_version])
42
+
43
+ opts.merge!(puppet_env[:gemset])
44
+
45
+ PDK::Util::Bundler.ensure_bundle!(puppet_env[:gemset])
46
+
36
47
  report = nil
37
48
 
38
49
  if opts[:list]
39
- examples = PDK::Test::Unit.list
50
+ examples = PDK::Test::Unit.list(opts)
51
+
40
52
  if examples.empty?
41
53
  puts _('No unit test files with examples were found.')
42
54
  else
@@ -66,15 +78,6 @@ module PDK::CLI
66
78
  }]
67
79
  end
68
80
 
69
- # Ensure that the bundled gems are up to date and correct Ruby is activated before running tests.
70
- puppet_env = PDK::CLI::Util.puppet_from_opts_or_env(opts)
71
- PDK::Util::PuppetVersion.fetch_puppet_dev if opts.key?(:'puppet-dev')
72
- PDK::Util::RubyVersion.use(puppet_env[:ruby_version])
73
-
74
- opts.merge!(puppet_env[:gemset])
75
-
76
- PDK::Util::Bundler.ensure_bundle!(puppet_env[:gemset])
77
-
78
81
  exit_code = PDK::Test::Unit.invoke(report, opts)
79
82
 
80
83
  report_formats.each do |format|
@@ -26,6 +26,27 @@ module PDK::CLI
26
26
  raise PDK::CLI::ExitWithError, _('You can not specify --noop and --force when updating a module')
27
27
  end
28
28
 
29
+ if Gem::Version.new(PDK::VERSION) < Gem::Version.new(PDK::Util.module_pdk_version)
30
+ PDK.logger.warn _(
31
+ 'This module has been updated to PDK %{module_pdk_version} which ' \
32
+ 'is newer than your PDK version (%{user_pdk_version}), proceed ' \
33
+ 'with caution!',
34
+ ) % {
35
+ module_pdk_version: PDK::Util.module_pdk_version,
36
+ user_pdk_version: PDK::VERSION,
37
+ }
38
+
39
+ unless opts[:force]
40
+ raise PDK::CLI::ExitWithError, _(
41
+ 'Please update your PDK installation and try again. ' \
42
+ 'You may also use the --force flag to override this and ' \
43
+ 'continue at your own risk.',
44
+ )
45
+ end
46
+ end
47
+
48
+ PDK::CLI::Util.analytics_screen_view('update', opts)
49
+
29
50
  updater = PDK::Module::Update.new(opts)
30
51
 
31
52
  updater.run
data/lib/pdk/cli/util.rb CHANGED
@@ -198,6 +198,41 @@ module PDK
198
198
  raise PDK::CLI::ExitWithError, _('--template-url may not be used to specify paths containing #\'s.')
199
199
  end
200
200
  module_function :validate_template_opts
201
+
202
+ def analytics_screen_view(screen_name, opts = {})
203
+ dimensions = {
204
+ ruby_version: RUBY_VERSION,
205
+ }
206
+
207
+ cmd_opts = opts.dup.reject do |_, v|
208
+ v.nil? || (v.respond_to?(:empty?) && v.empty?)
209
+ end
210
+
211
+ if (format_args = cmd_opts.delete(:format))
212
+ formats = PDK::CLI::Util::OptionNormalizer.report_formats(format_args)
213
+ dimensions[:output_format] = formats.map { |r| r[:method].to_s.gsub(%r{\Awrite_}, '') }.sort.uniq.join(',')
214
+ else
215
+ dimensions[:output_format] = 'default'
216
+ end
217
+
218
+ safe_opts = [:'puppet-version', :'pe-version']
219
+ redacted_opts = cmd_opts.map do |k, v|
220
+ value = if [true, false].include?(v) || safe_opts.include?(k)
221
+ v
222
+ else
223
+ 'redacted'
224
+ end
225
+ "#{k}=#{value}"
226
+ end
227
+ dimensions[:cli_options] = redacted_opts.join(',') unless redacted_opts.empty?
228
+
229
+ ignored_env_vars = %w[PDK_ANALYTICS_CONFIG PDK_DISABLE_ANALYTICS]
230
+ env_vars = ENV.select { |k, _| k.start_with?('PDK_') && !ignored_env_vars.include?(k) }.map { |k, v| "#{k}=#{v}" }
231
+ dimensions[:env_vars] = env_vars.join(',') unless env_vars.empty?
232
+
233
+ PDK.analytics.screen_view(screen_name, dimensions)
234
+ end
235
+ module_function :analytics_screen_view
201
236
  end
202
237
  end
203
238
  end
@@ -30,7 +30,13 @@ module PDK
30
30
  @prompt.print pastel.bold(_('[Q %{current_number}/%{questions_total}]') % { current_number: i, questions_total: num_questions }) + ' '
31
31
  @prompt.puts pastel.bold(question[:question])
32
32
  @prompt.puts question[:help] if question.key?(:help)
33
- if question.key?(:choices)
33
+
34
+ case question[:type]
35
+ when :yes
36
+ yes?(_('-->')) do |q|
37
+ q.default(question[:default]) if question.key?(:default)
38
+ end
39
+ when :multi_select
34
40
  multi_select(_('-->'), per_page: question[:choices].count) do |q|
35
41
  q.enum ')'
36
42
  q.default(*question[:default]) if question.key?(:default)
@@ -31,6 +31,7 @@ module PDK::CLI
31
31
  targets = []
32
32
 
33
33
  if opts[:list]
34
+ PDK::CLI::Util.analytics_screen_view('validate', opts)
34
35
  PDK.logger.info(_('Available validators: %{validator_names}') % { validator_names: validator_names.join(', ') })
35
36
  exit 0
36
37
  end
@@ -71,6 +72,12 @@ module PDK::CLI
71
72
  PDK.logger.info(_('Running all available validators...'))
72
73
  end
73
74
 
75
+ if validators == PDK::Validate.validators
76
+ PDK::CLI::Util.analytics_screen_view('validate', opts)
77
+ else
78
+ PDK::CLI::Util.analytics_screen_view(['validate', validators.map(&:name).sort].flatten.join('_'), opts)
79
+ end
80
+
74
81
  # Subsequent arguments are targets.
75
82
  targets.concat(args.to_a[1..-1]) if args.length > 1
76
83
 
@@ -85,11 +92,11 @@ module PDK::CLI
85
92
  end
86
93
 
87
94
  options = targets.empty? ? {} : { targets: targets }
88
- options[:auto_correct] = true if opts.key?(:'auto-correct')
95
+ options[:auto_correct] = true if opts[:'auto-correct']
89
96
 
90
97
  # Ensure that the bundled gems are up to date and correct Ruby is activated before running any validations.
91
98
  puppet_env = PDK::CLI::Util.puppet_from_opts_or_env(opts)
92
- PDK::Util::PuppetVersion.fetch_puppet_dev if opts.key?(:'puppet-dev')
99
+ PDK::Util::PuppetVersion.fetch_puppet_dev if opts[:'puppet-dev']
93
100
  PDK::Util::RubyVersion.use(puppet_env[:ruby_version])
94
101
 
95
102
  options.merge!(puppet_env[:gemset])
data/lib/pdk/config.rb ADDED
@@ -0,0 +1,94 @@
1
+ require 'pdk/config/errors'
2
+ require 'pdk/config/json'
3
+ require 'pdk/config/namespace'
4
+ require 'pdk/config/validator'
5
+ require 'pdk/config/value'
6
+ require 'pdk/config/yaml'
7
+
8
+ module PDK
9
+ def self.config
10
+ @config ||= PDK::Config.new
11
+ end
12
+
13
+ class Config
14
+ def user
15
+ @user ||= PDK::Config::JSON.new('user', file: PDK::Config.user_config_path) do
16
+ mount :module_defaults, PDK::Config::JSON.new(file: PDK.answers.answer_file_path)
17
+
18
+ mount :analytics, PDK::Config::YAML.new(file: PDK::Config.analytics_config_path) do
19
+ value :disabled do
20
+ validate PDK::Config::Validator.boolean
21
+ default_to { PDK::Config.bolt_analytics_config.fetch('disabled', true) }
22
+ end
23
+
24
+ value 'user-id' do
25
+ validate PDK::Config::Validator.uuid
26
+ default_to do
27
+ require 'securerandom'
28
+
29
+ PDK::Config.bolt_analytics_config.fetch('user-id', SecureRandom.uuid)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.bolt_analytics_config
37
+ PDK::Config::YAML.new(file: File.expand_path('~/.puppetlabs/bolt/analytics.yaml'))
38
+ end
39
+
40
+ def self.analytics_config_path
41
+ ENV['PDK_ANALYTICS_CONFIG'] || File.join(File.dirname(PDK::Util.configdir), 'puppet', 'analytics.yml')
42
+ end
43
+
44
+ def self.user_config_path
45
+ File.join(PDK::Util.configdir, 'user_config.json')
46
+ end
47
+
48
+ def self.analytics_config_exist?
49
+ PDK::Util::Filesystem.file?(analytics_config_path)
50
+ end
51
+
52
+ def self.analytics_config_interview!
53
+ return unless PDK::CLI::Util.interactive?
54
+
55
+ pre_message = _(
56
+ 'PDK collects anonymous usage information to help us understand how ' \
57
+ 'it is being used and make decisions on how to improve it. You can ' \
58
+ 'find out more about what data we collect and how it is used in the ' \
59
+ "PDK documentation at %{url}.\n",
60
+ ) % { url: 'https://puppet.com/docs/pdk/latest/pdk_install.html' }
61
+ post_message = _(
62
+ 'You can opt in or out of the usage data collection at any time by ' \
63
+ 'editing the analytics configuration file at %{path} and changing ' \
64
+ "the '%{key}' value.",
65
+ ) % {
66
+ path: PDK::Config.analytics_config_path,
67
+ key: 'disabled',
68
+ }
69
+
70
+ questions = [
71
+ {
72
+ name: 'enabled',
73
+ question: _('Do you consent to the collection of anonymous PDK usage information?'),
74
+ type: :yes,
75
+ },
76
+ ]
77
+
78
+ PDK.logger.info(text: pre_message, wrap: true)
79
+ prompt = TTY::Prompt.new(help_color: :cyan)
80
+ interview = PDK::CLI::Util::Interview.new(prompt)
81
+ interview.add_questions(questions)
82
+ answers = interview.run
83
+
84
+ if answers.nil?
85
+ PDK.logger.info _('No answer given, opting out of analytics collection.')
86
+ PDK.config.user['analytics']['disabled'] = true
87
+ else
88
+ PDK.config.user['analytics']['disabled'] = !answers['enabled']
89
+ end
90
+
91
+ PDK.logger.info(text: post_message, wrap: true)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,5 @@
1
+ module PDK
2
+ class Config
3
+ class LoadError < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,23 @@
1
+ require 'pdk/config/namespace'
2
+
3
+ module PDK
4
+ class Config
5
+ class JSON < Namespace
6
+ def parse_data(data, _filename)
7
+ return {} if data.nil? || data.empty?
8
+
9
+ require 'json'
10
+
11
+ ::JSON.parse(data)
12
+ rescue ::JSON::ParserError => e
13
+ raise PDK::Config::LoadError, e.message
14
+ end
15
+
16
+ def serialize_data(data)
17
+ require 'json'
18
+
19
+ ::JSON.pretty_generate(data)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,273 @@
1
+ module PDK
2
+ class Config
3
+ class Namespace
4
+ # @param value [String] the new name of this namespace.
5
+ attr_writer :name
6
+
7
+ # @return [String] the path to the file associated with the contents of
8
+ # this namespace.
9
+ attr_reader :file
10
+
11
+ # @return [self] the parent namespace of this namespace.
12
+ attr_accessor :parent
13
+
14
+ # Initialises the PDK::Config::Namespace object.
15
+ #
16
+ # @param name [String] the name of the namespace (defaults to nil).
17
+ # @param params [Hash{Symbol => Object}] keyword parameters for the
18
+ # method.
19
+ # @option params [String] :file the path to the file associated with the
20
+ # contents of the namespace (defaults to nil).
21
+ # @option params [self] :parent the parent {self} that this namespace is
22
+ # a child of (defaults to nil).
23
+ # @param block [Proc] a block that is evaluated within the new instance.
24
+ def initialize(name = nil, file: nil, parent: nil, &block)
25
+ @file = File.expand_path(file) unless file.nil?
26
+ @values = {}
27
+ @name = name.to_s
28
+ @parent = parent
29
+
30
+ instance_eval(&block) if block_given?
31
+ end
32
+
33
+ # Pre-configure a value in the namespace.
34
+ #
35
+ # Allows you to specify validators and a default value for value in the
36
+ # namespace (see PDK::Config::Value#initialize).
37
+ #
38
+ # @param key [String,Symbol] the name of the value.
39
+ # @param block [Proc] a block that is evaluated within the new [self].
40
+ #
41
+ # @return [nil]
42
+ def value(key, &block)
43
+ @values[key.to_s] ||= PDK::Config::Value.new(key.to_s)
44
+ @values[key.to_s].instance_eval(&block) if block_given?
45
+ end
46
+
47
+ # Mount a provided [self] (or subclass) into the namespace.
48
+ #
49
+ # @param key [String,Symbol] the name of the namespace to be mounted.
50
+ # @param obj [self] the namespace to be mounted.
51
+ # @param block [Proc] a block to be evaluated within the instance of the
52
+ # newly mounted namespace.
53
+ #
54
+ # @raise [ArgumentError] if the object to be mounted is not a {self} or
55
+ # subclass thereof.
56
+ #
57
+ # @return [self] the mounted namespace.
58
+ def mount(key, obj, &block)
59
+ raise ArgumentError, _('Only PDK::Config::Namespace objects can be mounted into a namespace') unless obj.is_a?(PDK::Config::Namespace)
60
+ obj.parent = self
61
+ obj.name = key.to_s
62
+ obj.instance_eval(&block) if block_given?
63
+ data[key.to_s] = obj
64
+ end
65
+
66
+ # Create and mount a new child namespace.
67
+ #
68
+ # @param name [String,Symbol] the name of the new namespace.
69
+ # @param block [Proc]
70
+ def namespace(name, &block)
71
+ mount(name, PDK::Config::Namespace.new, &block)
72
+ end
73
+
74
+ # Get the value of the named key.
75
+ #
76
+ # If there is a value for that key, return it. If not, follow the logic
77
+ # described in {#default_config_value} to determine the default value to
78
+ # return.
79
+ #
80
+ # @note Unlike a Ruby Hash, this will not return `nil` in the event that
81
+ # the key does not exist (see #fetch).
82
+ #
83
+ # @param key [String,Symbol] the name of the value to retrieve.
84
+ #
85
+ # @return [Object] the requested value.
86
+ def [](key)
87
+ data[key.to_s]
88
+ end
89
+
90
+ # Get the value of the named key or the provided default value if not
91
+ # present.
92
+ #
93
+ # This differs from {#[]} in an important way in that it allows you to
94
+ # return a default value, which is not possible using `[] || default` as
95
+ # non-existent values when accessed normally via {#[]} will be defaulted
96
+ # to a new Hash.
97
+ #
98
+ # @param key [String,Symbol] the name of the value to fetch.
99
+ # @param default_value [Object] the value to return if the namespace does
100
+ # not contain the requested value.
101
+ #
102
+ # @return [Object] the requested value.
103
+ def fetch(key, default_value)
104
+ data.fetch(key.to_s, default_value)
105
+ end
106
+
107
+ # Set the value of the named key.
108
+ #
109
+ # If the key has been pre-configured with {#value}, then the value of the
110
+ # key will be validated against any validators that have been configured.
111
+ #
112
+ # After the value has been set in memory, the value will then be
113
+ # persisted to disk.
114
+ #
115
+ # @param key [String,Symbol] the name of the configuration value.
116
+ # @param value [Object] the value of the configuration value.
117
+ #
118
+ # @return [nil]
119
+ def []=(key, value)
120
+ @values[key.to_s].validate!([name, key.to_s].join('.'), value) if @values.key?(key.to_s)
121
+
122
+ data[key.to_s] = value
123
+ save_data
124
+ end
125
+
126
+ # Convert the namespace into a Hash of values, suitable for serialising
127
+ # and persisting to disk.
128
+ #
129
+ # Child namespaces that are associated with their own files are excluded
130
+ # from the Hash (as their values will be persisted to their own files)
131
+ # and nil values are removed from the Hash.
132
+ #
133
+ # @return [Hash{String => Object}] the values from the namespace that
134
+ # should be persisted to disk.
135
+ def to_h
136
+ data.inject({}) do |new_hash, (key, value)|
137
+ new_hash[key] = if value.is_a?(PDK::Config::Namespace)
138
+ value.include_in_parent? ? value.to_h : nil
139
+ else
140
+ value
141
+ end
142
+ new_hash.delete_if { |_, v| v.nil? }
143
+ end
144
+ end
145
+
146
+ # @return [Boolean] true if the namespace has a parent, otherwise false.
147
+ def child_namespace?
148
+ !parent.nil?
149
+ end
150
+
151
+ # Determines the fully qualified name of the namespace.
152
+ #
153
+ # If this is a child namespace, then fully qualified name for the
154
+ # namespace will be "<parent>.<child>".
155
+ #
156
+ # @return [String] the fully qualifed name of the namespace.
157
+ def name
158
+ child_namespace? ? [parent.name, @name].join('.') : @name
159
+ end
160
+
161
+ # Determines if the contents of the namespace should be included in the
162
+ # parent namespace when persisting to disk.
163
+ #
164
+ # If the namespace has been mounted into a parent namespace and is not
165
+ # associated with its own file on disk, then the values in the namespace
166
+ # should be included in the parent namespace when persisting to disk.
167
+ #
168
+ # @return [Boolean] true if the values should be included in the parent
169
+ # namespace.
170
+ def include_in_parent?
171
+ child_namespace? && file.nil?
172
+ end
173
+
174
+ private
175
+
176
+ # @abstract Subclass and override {#parse_data} to implement parsing logic
177
+ # for a particular config file format.
178
+ #
179
+ # @param data [String] The content of the file to be parsed.
180
+ # @param filename [String] The path to the file to be parsed.
181
+ #
182
+ # @return [Hash{String => Object}] the data to be loaded into the
183
+ # namespace.
184
+ def parse_data(_data, _filename)
185
+ {}
186
+ end
187
+
188
+ # Read the file associated with the namespace.
189
+ #
190
+ # @raise [PDK::Config::LoadError] if the file is removed during read.
191
+ # @raise [PDK::Config::LoadError] if the user doesn't have the
192
+ # permissions needed to read the file.
193
+ # @return [String,nil] the contents of the file or nil if the file does
194
+ # not exist.
195
+ def load_data
196
+ return if file.nil?
197
+ return unless PDK::Util::Filesystem.file?(file)
198
+
199
+ PDK::Util::Filesystem.read_file(file)
200
+ rescue Errno::ENOENT => e
201
+ raise PDK::Config::LoadError, e.message
202
+ rescue Errno::EACCES
203
+ raise PDK::Config::LoadError, _('Unable to open %{file} for reading') % {
204
+ file: file,
205
+ }
206
+ end
207
+
208
+ # @abstract Subclass and override {#save_data} to implement generating
209
+ # logic for a particular config file format.
210
+ #
211
+ # @param data [Hash{String => Object}] the data stored in the namespace
212
+ #
213
+ # @return [String] the serialized contents of the namespace suitable for
214
+ # writing to disk.
215
+ def serialize_data(_data); end
216
+
217
+ # Persist the contents of the namespace to disk.
218
+ #
219
+ # Directories will be automatically created and the contents of the
220
+ # namespace will be serialized automatically with {#serialize_data}.
221
+ #
222
+ # @raise [PDK::Config::LoadError] if one of the intermediary path components
223
+ # exist but is not a directory.
224
+ # @raise [PDK::Config::LoadError] if the user does not have the
225
+ # permissions needed to write the file.
226
+ #
227
+ # @return [nil]
228
+ def save_data
229
+ return if file.nil?
230
+
231
+ FileUtils.mkdir_p(File.dirname(file))
232
+
233
+ PDK::Util::Filesystem.write_file(file, serialize_data(to_h))
234
+ rescue Errno::EACCES
235
+ raise PDK::Config::LoadError, _('Unable to open %{file} for writing') % {
236
+ file: file,
237
+ }
238
+ rescue SystemCallError => e
239
+ raise PDK::Config::LoadError, e.message
240
+ end
241
+
242
+ # Memoised accessor for the loaded data.
243
+ #
244
+ # @return [Hash<String => Object>] the contents of the namespace.
245
+ def data
246
+ @data ||= parse_data(load_data, file).tap do |h|
247
+ h.default_proc = default_config_value
248
+ end
249
+ end
250
+
251
+ # The default behaviour of the namespace when the requested value does
252
+ # not exist.
253
+ #
254
+ # If the value has been pre-configured with {#value} to have a default
255
+ # value, resolve the default value and set it in the namespace
256
+ # (triggering a call to {#save_data}. Otherwise, set the value to a new
257
+ # Hash to allow for arbitrary level of nested values.
258
+ #
259
+ # @return [Proc] suitable for use by {Hash#default_proc}.
260
+ def default_config_value
261
+ ->(hash, key) do
262
+ if @values.key?(key) && @values[key].default?
263
+ self[key] = @values[key].default
264
+ else
265
+ hash[key] = {}.tap do |h|
266
+ h.default_proc = default_config_value
267
+ end
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end