pdk 1.10.0 → 1.11.0

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