pdk 1.13.0 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +25 -0
  3. data/README.md +41 -0
  4. data/lib/pdk.rb +0 -13
  5. data/lib/pdk/analytics.rb +18 -2
  6. data/lib/pdk/analytics/client/google_analytics.rb +3 -0
  7. data/lib/pdk/answer_file.rb +4 -1
  8. data/lib/pdk/cli.rb +7 -2
  9. data/lib/pdk/cli/build.rb +2 -2
  10. data/lib/pdk/cli/bundle.rb +7 -1
  11. data/lib/pdk/cli/console.rb +148 -0
  12. data/lib/pdk/cli/convert.rb +2 -2
  13. data/lib/pdk/cli/exec.rb +14 -14
  14. data/lib/pdk/cli/exec/command.rb +16 -11
  15. data/lib/pdk/cli/exec/interactive_command.rb +4 -0
  16. data/lib/pdk/cli/exec_group.rb +5 -5
  17. data/lib/pdk/cli/module/build.rb +0 -2
  18. data/lib/pdk/cli/module/generate.rb +1 -2
  19. data/lib/pdk/cli/new.rb +1 -1
  20. data/lib/pdk/cli/new/defined_type.rb +2 -0
  21. data/lib/pdk/cli/new/provider.rb +2 -0
  22. data/lib/pdk/cli/new/task.rb +2 -0
  23. data/lib/pdk/cli/new/{unit_test.rb → test.rb} +16 -12
  24. data/lib/pdk/cli/new/transport.rb +2 -0
  25. data/lib/pdk/cli/test/unit.rb +5 -3
  26. data/lib/pdk/cli/update.rb +2 -3
  27. data/lib/pdk/cli/util.rb +45 -14
  28. data/lib/pdk/cli/util/spinner.rb +2 -2
  29. data/lib/pdk/cli/validate.rb +6 -2
  30. data/lib/pdk/config.rb +20 -8
  31. data/lib/pdk/config/analytics_schema.json +26 -0
  32. data/lib/pdk/config/json.rb +14 -3
  33. data/lib/pdk/config/json_schema_namespace.rb +143 -0
  34. data/lib/pdk/config/json_schema_setting.rb +53 -0
  35. data/lib/pdk/config/json_with_schema.rb +50 -0
  36. data/lib/pdk/config/namespace.rb +84 -76
  37. data/lib/pdk/config/setting.rb +132 -0
  38. data/lib/pdk/config/yaml.rb +15 -3
  39. data/lib/pdk/config/yaml_with_schema.rb +59 -0
  40. data/lib/pdk/generate.rb +0 -2
  41. data/lib/pdk/generate/module.rb +29 -16
  42. data/lib/pdk/generate/puppet_object.rb +31 -28
  43. data/lib/pdk/module.rb +2 -2
  44. data/lib/pdk/module/build.rb +21 -8
  45. data/lib/pdk/module/convert.rb +64 -7
  46. data/lib/pdk/module/metadata.rb +5 -1
  47. data/lib/pdk/module/templatedir.rb +24 -7
  48. data/lib/pdk/module/update.rb +5 -1
  49. data/lib/pdk/module/update_manager.rb +21 -13
  50. data/lib/pdk/report.rb +4 -3
  51. data/lib/pdk/report/event.rb +5 -3
  52. data/lib/pdk/tests/unit.rb +36 -7
  53. data/lib/pdk/util.rb +20 -8
  54. data/lib/pdk/util/bundler.rb +14 -6
  55. data/lib/pdk/util/filesystem.rb +5 -0
  56. data/lib/pdk/util/git.rb +6 -0
  57. data/lib/pdk/util/puppet_strings.rb +24 -2
  58. data/lib/pdk/util/puppet_version.rb +25 -10
  59. data/lib/pdk/util/ruby_version.rb +13 -1
  60. data/lib/pdk/util/template_uri.rb +23 -2
  61. data/lib/pdk/util/vendored_file.rb +28 -24
  62. data/lib/pdk/util/version.rb +5 -5
  63. data/lib/pdk/validate/base_validator.rb +5 -4
  64. data/lib/pdk/validate/metadata/metadata_json_lint.rb +0 -4
  65. data/lib/pdk/validate/metadata/metadata_syntax.rb +5 -3
  66. data/lib/pdk/validate/metadata_validator.rb +0 -2
  67. data/lib/pdk/validate/puppet/puppet_epp.rb +4 -4
  68. data/lib/pdk/validate/puppet/puppet_lint.rb +0 -3
  69. data/lib/pdk/validate/puppet/puppet_syntax.rb +4 -4
  70. data/lib/pdk/validate/puppet_validator.rb +0 -2
  71. data/lib/pdk/validate/ruby/rubocop.rb +0 -5
  72. data/lib/pdk/validate/ruby_validator.rb +0 -2
  73. data/lib/pdk/validate/tasks/metadata_lint.rb +9 -5
  74. data/lib/pdk/validate/tasks/name.rb +4 -2
  75. data/lib/pdk/validate/tasks_validator.rb +0 -2
  76. data/lib/pdk/validate/yaml/syntax.rb +4 -4
  77. data/lib/pdk/validate/yaml_validator.rb +0 -2
  78. data/lib/pdk/version.rb +1 -1
  79. data/locales/pdk.pot +351 -311
  80. metadata +11 -7
  81. data/lib/pdk/config/validator.rb +0 -31
  82. data/lib/pdk/config/value.rb +0 -94
@@ -0,0 +1,26 @@
1
+ {
2
+ "definitions": {},
3
+ "$schema": "http://json-schema.org/draft-06/schema#",
4
+ "$id": "http://puppet.com/schema/does_not_exist.json",
5
+ "type": "object",
6
+ "title": "The PDK Analytics YAML Schema",
7
+ "properties": {
8
+ "disabled": {
9
+ "$id": "#/properties/disabled",
10
+ "type": "boolean",
11
+ "title": "Disabled property",
12
+ "examples": [
13
+ false
14
+ ]
15
+ },
16
+ "user-id": {
17
+ "$id": "#/properties/user-id",
18
+ "type": "string",
19
+ "title": "The User-id for analytics",
20
+ "examples": [
21
+ "cb9ed65f-37dc-48d8-9863-8bd09cbb61c7"
22
+ ],
23
+ "pattern": "^[0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{12}$"
24
+ }
25
+ }
26
+ }
@@ -3,16 +3,27 @@ require 'pdk/config/namespace'
3
3
  module PDK
4
4
  class Config
5
5
  class JSON < Namespace
6
- def parse_data(data, _filename)
7
- return {} if data.nil? || data.empty?
6
+ # Parses a JSON document.
7
+ #
8
+ # @see PDK::Config::Namespace.parse_file
9
+ def parse_file(filename)
10
+ raise unless block_given?
11
+ data = load_data(filename)
12
+ return if data.nil? || data.empty?
8
13
 
9
14
  require 'json'
10
15
 
11
- ::JSON.parse(data)
16
+ data = ::JSON.parse(data)
17
+ return if data.nil? || data.empty?
18
+
19
+ data.each { |k, v| yield k, PDK::Config::Setting.new(k, self, v) }
12
20
  rescue ::JSON::ParserError => e
13
21
  raise PDK::Config::LoadError, e.message
14
22
  end
15
23
 
24
+ # Serializes object data into a JSON string.
25
+ #
26
+ # @see PDK::Config::Namespace.serialize_data
16
27
  def serialize_data(data)
17
28
  require 'json'
18
29
 
@@ -0,0 +1,143 @@
1
+ require 'pdk/config/namespace'
2
+
3
+ # Due to https://github.com/ruby-json-schema/json-schema/issues/439
4
+ # Windows file paths "appear" as uri's with no host and a schema of drive letter
5
+ # Also it is not possible to craft a URI with a Windows path due to the URI object
6
+ # always prepending the path with forward slash (`/`) so Windows paths end up looking
7
+ # like '/C:\schema.json', which can not be read.
8
+ # Instead we need to monkey patch the Schema Reader reader to remove the errant forward slash
9
+ require 'json-schema/schema/reader'
10
+ module JSON
11
+ class Schema
12
+ class Reader
13
+ alias original_read_file read_file
14
+
15
+ def read_file(pathname)
16
+ new_pathname = JSON::Util::URI.unescaped_path(pathname.to_s)
17
+ # Munge the path if it looks like a Windows path e.g. /C:/Windows ...
18
+ # Note that UNC style paths do not have the same issue (\\host\path)
19
+ new_pathname.slice!(0) if new_pathname.start_with?('/') && new_pathname[2] == ':'
20
+ original_read_file(Pathname.new(new_pathname))
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ module PDK
27
+ class Config
28
+ class JSONSchemaNamespace < Namespace
29
+ # Initialises the PDK::Config::JSONSchemaNamespace object.
30
+ #
31
+ # @see PDK::Config::Namespace.initialize
32
+ #
33
+ # @option params [String] :schema_file Path to the JSON Schema document
34
+ def initialize(name = nil, file: nil, parent: nil, persistent_defaults: false, schema_file: nil, &block)
35
+ super(name, file: file, parent: parent, persistent_defaults: persistent_defaults, &block)
36
+ @schema_file = schema_file
37
+ @unmanaged_settings = {}
38
+ end
39
+
40
+ # The JSON Schema for the namespace
41
+ #
42
+ # @return [Hash]
43
+ def schema
44
+ document_schema.schema
45
+ end
46
+
47
+ # Whether the schema is valid but empty.
48
+ #
49
+ # @return [Boolean]
50
+ def empty_schema?
51
+ document_schema.schema.empty?
52
+ end
53
+
54
+ # Name of all the top level properties for the schema
55
+ #
56
+ # @return [String[]]
57
+ def schema_property_names
58
+ return [] if schema['properties'].nil?
59
+ schema['properties'].keys
60
+ end
61
+
62
+ # Extends the to_h namespace method to include unmanaged settings
63
+ #
64
+ # @see PDK::Config::Namespace.to_h
65
+ def to_h
66
+ # This may seem counter-intuitive but we need to call super first as the settings
67
+ # may not have been loaded yet, which means @unmanaged_settings will be empty.
68
+ # We call super first to force any file loading and then merge the unmanaged settings
69
+ settings_hash = super
70
+ @unmanaged_settings = {} if @unmanaged_settings.nil?
71
+ @unmanaged_settings.merge(settings_hash)
72
+ end
73
+
74
+ # Validates a document (Hash table) against the schema
75
+ #
76
+ # @return [Boolean]
77
+ def validate_document!(document)
78
+ ::JSON::Validator.validate!(schema, document)
79
+ end
80
+
81
+ protected
82
+
83
+ # @!attribute [w] unmanaged_settings
84
+ # Sets the list of unmanaged settings. For subclass use only
85
+ #
86
+ # @param unmanaged_settings [Hash<String, Object]] A hashtable of all unmanaged settings which will be persisted, but not visible
87
+ # @protected
88
+ attr_writer :unmanaged_settings
89
+
90
+ private
91
+
92
+ # Override the create_setting method to always fail. This is called
93
+ # to dyanmically add settings. However as we're using a schema, no
94
+ # new settings can be created
95
+ #
96
+ # @see PDK::Config::Namespace.create_missing_setting
97
+ #
98
+ # @private
99
+ def create_missing_setting(key, _initial_value = nil)
100
+ raise ArgumentError, _("Setting '#{key}' does not exist'")
101
+ end
102
+
103
+ # Create a valid, but empty schema
104
+ #
105
+ # @return [JSON::Schema]
106
+ def create_empty_schema
107
+ require 'json-schema'
108
+ ::JSON::Schema.new({}, 'http://json-schema.org/draft-06/schema#')
109
+ end
110
+
111
+ # Lazily retrieve the JSON schema from disk for this namespace
112
+ #
113
+ # @return [JSON::Schema]
114
+ def document_schema
115
+ return @document_schema unless @document_schema.nil?
116
+
117
+ # Create an empty schema by default.
118
+ @document_schema = create_empty_schema
119
+
120
+ require 'json-schema'
121
+
122
+ return @document_schema if @schema_file.nil?
123
+ unless PDK::Util::Filesystem.file?(@schema_file)
124
+ raise PDK::Config::LoadError, _('Unable to open %{file} for reading. File does not exist') % {
125
+ file: @schema_file,
126
+ }
127
+ end
128
+
129
+ # The schema should not query external URI references, except for the meta-schema. Local files are allowed
130
+ schema_reader = ::JSON::Schema::Reader.new(
131
+ accept_file: true,
132
+ accept_uri: proc { |uri| uri.host.nil? || ['json-schema.org'].include?(uri.host) },
133
+ )
134
+ @document_schema = schema_reader.read(Addressable::URI.convert_path(@schema_file))
135
+ rescue ::JSON::Schema::JsonParseError => e
136
+ raise PDK::Config::LoadError, _('Unable to open %{file} for reading. JSON Error: %{msg}') % {
137
+ file: @schema_file,
138
+ msg: e.message,
139
+ }
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,53 @@
1
+ require 'pdk/config/json_schema_namespace'
2
+
3
+ module PDK
4
+ class Config
5
+ class JSONSchemaSetting < PDK::Config::Setting
6
+ # Initialises the PDK::Config::JSONSchemaSetting object.
7
+ #
8
+ # @see PDK::Config::Setting.initialize
9
+ def initialize(_name, namespace, _initial_value)
10
+ raise 'The JSONSchemaSetting object can only be created within the JSONSchemaNamespace' unless namespace.is_a?(PDK::Config::JSONSchemaNamespace)
11
+ super
12
+ end
13
+
14
+ # Verifies that the new setting value is valid by calling the JSON schema validator on
15
+ # a hash which includes the new setting
16
+ #
17
+ # @see PDK::Config::Setting.validate!
18
+ def validate!(value)
19
+ # Get the existing namespace data
20
+ new_document = namespace.to_h
21
+ # ... set the new value
22
+ new_document[@name] = value
23
+ begin
24
+ # ... add validate it
25
+ namespace.validate_document!(new_document)
26
+ rescue ::JSON::Schema::ValidationError => e
27
+ raise ArgumentError, _('%{key} %{message}') % {
28
+ key: qualified_name,
29
+ message: e.message,
30
+ }
31
+ end
32
+ end
33
+
34
+ # Evaluate the default setting, firstly from the JSON schema and then
35
+ # from any other default evaluators in the settings chain.
36
+ #
37
+ # @see PDK::Config::Setting.default
38
+ #
39
+ # @return [Object, nil] the result of evaluating the block given to
40
+ # {#default_to}, or `nil` if the setting has no default.
41
+ def default
42
+ # Return the default from the schema document if it exists
43
+ if namespace.schema_property_names.include?(@name)
44
+ prop_schema = namespace.schema['properties'][@name]
45
+ return prop_schema['default'] unless prop_schema['default'].nil?
46
+ end
47
+ # ... otherwise call the settings chain default
48
+ # and if that doesn't exist, just return nil
49
+ @previous_setting.nil? ? nil : @previous_setting.default
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ require 'pdk/config/json_schema_namespace'
2
+ require 'pdk/config/json_schema_setting'
3
+
4
+ module PDK
5
+ class Config
6
+ class JSONWithSchema < JSONSchemaNamespace
7
+ # Parses a JSON document with a schema.
8
+ #
9
+ # @see PDK::Config::Namespace.parse_file
10
+ def parse_file(filename)
11
+ raise unless block_given?
12
+ data = load_data(filename)
13
+ data = '{}' if data.nil? || data.empty?
14
+ require 'json'
15
+
16
+ @raw_data = ::JSON.parse(data)
17
+ @raw_data = {} if @raw_data.nil?
18
+
19
+ begin
20
+ # Ensure the parsed document is actually valid
21
+ validate_document!(@raw_data)
22
+ rescue ::JSON::Schema::ValidationError => e
23
+ raise PDK::Config::LoadError, _('The configuration file %{filename} is not valid: %{message}') % {
24
+ filename: filename,
25
+ message: e.message,
26
+ }
27
+ end
28
+
29
+ schema_property_names.each do |key|
30
+ yield key, PDK::Config::JSONSchemaSetting.new(key, self, @raw_data[key])
31
+ end
32
+
33
+ # Remove all of the "known" settings from the schema and
34
+ # we're left with the settings that we don't manage.
35
+ self.unmanaged_settings = @raw_data.reject { |k, _| schema_property_names.include?(k) }
36
+ rescue ::JSON::ParserError => e
37
+ raise PDK::Config::LoadError, e.message
38
+ end
39
+
40
+ # Serializes object data into a JSON string.
41
+ #
42
+ # @see PDK::Config::Namespace.serialize_data
43
+ def serialize_data(data)
44
+ require 'json'
45
+
46
+ ::JSON.pretty_generate(data)
47
+ end
48
+ end
49
+ end
50
+ end
@@ -26,10 +26,12 @@ module PDK
26
26
  # @param block [Proc] a block that is evaluated within the new instance.
27
27
  def initialize(name = nil, file: nil, parent: nil, persistent_defaults: false, &block)
28
28
  @file = File.expand_path(file) unless file.nil?
29
- @values = {}
29
+ @settings = {}
30
30
  @name = name.to_s
31
31
  @parent = parent
32
32
  @persistent_defaults = persistent_defaults
33
+ @mounts = {}
34
+ @loaded_from_file = false
33
35
 
34
36
  instance_eval(&block) if block_given?
35
37
  end
@@ -43,9 +45,9 @@ module PDK
43
45
  # @param block [Proc] a block that is evaluated within the new [self].
44
46
  #
45
47
  # @return [nil]
46
- def value(key, &block)
47
- @values[key.to_s] ||= PDK::Config::Value.new(key.to_s)
48
- @values[key.to_s].instance_eval(&block) if block_given?
48
+ def setting(key, &block)
49
+ @settings[key.to_s] ||= PDK::Config::Setting.new(key.to_s, self)
50
+ @settings[key.to_s].instance_eval(&block) if block_given?
49
51
  end
50
52
 
51
53
  # Mount a provided [self] (or subclass) into the namespace.
@@ -64,7 +66,7 @@ module PDK
64
66
  obj.parent = self
65
67
  obj.name = key.to_s
66
68
  obj.instance_eval(&block) if block_given?
67
- data[key.to_s] = obj
69
+ @mounts[key.to_s] = obj
68
70
  end
69
71
 
70
72
  # Create and mount a new child namespace.
@@ -88,11 +90,21 @@ module PDK
88
90
  #
89
91
  # @return [Object] the requested value.
90
92
  def [](key)
91
- data[key.to_s]
93
+ # Check if it's a mount first...
94
+ return @mounts[key.to_s] unless @mounts[key.to_s].nil?
95
+ # Check if it's a setting, otherwise nil
96
+ return nil if settings[key.to_s].nil?
97
+ return settings[key.to_s].value unless settings[key.to_s].value.nil?
98
+ default_value = settings[key.to_s].default
99
+ return default_value if default_value.nil? || !@persistent_defaults
100
+ # Persist the default value
101
+ settings[key.to_s].value = default_value
102
+ save_data
103
+ default_value
92
104
  end
93
105
 
94
106
  # Get the value of the named key or the provided default value if not
95
- # present.
107
+ # present. Note that this does not trigger persistent defaults
96
108
  #
97
109
  # This differs from {#[]} in an important way in that it allows you to
98
110
  # return a default value, which is not possible using `[] || default` as
@@ -105,7 +117,12 @@ module PDK
105
117
  #
106
118
  # @return [Object] the requested value.
107
119
  def fetch(key, default_value)
108
- data.fetch(key.to_s, default_value)
120
+ # Check if it's a mount first...
121
+ return @mounts[key.to_s] unless @mounts[key.to_s].nil?
122
+ # Check if it's a setting, otherwise default_value
123
+ return default_value if settings[key.to_s].nil?
124
+ # Check if has a value, otherwise default_value
125
+ settings[key.to_s].value.nil? ? default_value : settings[key.to_s].value
109
126
  end
110
127
 
111
128
  # After the value has been set in memory, the value will then be
@@ -116,6 +133,8 @@ module PDK
116
133
  #
117
134
  # @return [nil]
118
135
  def []=(key, value)
136
+ # You can't set the value of a mount
137
+ raise ArgumentError, _('Namespace mounts can not be set a value') unless @mounts[key.to_s].nil?
119
138
  set_volatile_value(key, value)
120
139
  # Persist the change
121
140
  save_data
@@ -131,14 +150,11 @@ module PDK
131
150
  # @return [Hash{String => Object}] the values from the namespace that
132
151
  # should be persisted to disk.
133
152
  def to_h
134
- data.inject({}) do |new_hash, (key, value)|
135
- new_hash[key] = if value.is_a?(PDK::Config::Namespace)
136
- value.include_in_parent? ? value.to_h : nil
137
- else
138
- value
139
- end
140
- new_hash.delete_if { |_, v| v.nil? }
141
- end
153
+ new_hash = {}
154
+ settings.each_pair { |k, v| new_hash[k] = v.value }
155
+ @mounts.each_pair { |k, mount_point| new_hash[k] = mount_point.to_h if mount_point.include_in_parent? }
156
+ new_hash.delete_if { |_, v| v.nil? }
157
+ new_hash
142
158
  end
143
159
 
144
160
  # Resolves all filtered settings, including child namespaces, fully namespaced and filling in default values.
@@ -146,20 +162,16 @@ module PDK
146
162
  # @param filter [String] Only resolve setting names which match the filter. See #be_resolved? for matching rules
147
163
  # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'}
148
164
  def resolve(filter = nil)
149
- # Explicitly force values to be loaded if they have not already
150
- # done so. This will not cause them to be persisted to disk
151
- (@values.keys - data.keys).each { |key_name| self[key_name] }
152
165
  resolved = {}
153
- data.each do |data_name, obj|
154
- case obj
155
- when PDK::Config::Namespace
156
- # Query the child namespace
157
- resolved.merge!(obj.resolve(filter))
158
- else
159
- setting_name = [name, data_name.to_s].join('.')
160
- resolved[setting_name] = self[data_name] if be_resolved?(setting_name, filter)
166
+ # Resolve the settings
167
+ settings.values.each do |setting|
168
+ setting_name = setting.qualified_name
169
+ if be_resolved?(setting_name, filter)
170
+ resolved[setting_name] = setting.value.nil? ? setting.default : setting.value
161
171
  end
162
172
  end
173
+ # Resolve the mounts
174
+ @mounts.values.each { |mount| resolved.merge!(mount.resolve(filter)) }
163
175
  resolved
164
176
  end
165
177
 
@@ -208,16 +220,38 @@ module PDK
208
220
  name.start_with?(filter + '.') # If name is a subkey of the filter then it should be resolved
209
221
  end
210
222
 
211
- # @abstract Subclass and override {#parse_data} to implement parsing logic
223
+ # @abstract Subclass and override {#parse_file} to implement parsing logic
212
224
  # for a particular config file format.
213
225
  #
214
226
  # @param data [String] The content of the file to be parsed.
215
227
  # @param filename [String] The path to the file to be parsed.
216
228
  #
217
- # @return [Hash{String => Object}] the data to be loaded into the
229
+ # @yield [String, Object] the data to be loaded into the
218
230
  # namespace.
219
- def parse_data(_data, _filename)
220
- {}
231
+ def parse_file(_filename); end
232
+
233
+ # @abstract Subclass and override {#serialize_data} to implement generating
234
+ # logic for a particular config file format.
235
+ #
236
+ # @param data [Hash{String => Object}] the data stored in the namespace
237
+ #
238
+ # @return [String] the serialized contents of the namespace suitable for
239
+ # writing to disk.
240
+ def serialize_data(_data); end
241
+
242
+ # @abstract Subclass and override {#create_missing_setting} to implement logic
243
+ # when a setting is dynamically created, for example when attempting to
244
+ # set the value of an unknown setting
245
+ #
246
+ # @param data [Hash{String => Object}] the data stored in the namespace
247
+ #
248
+ # @return [String] the serialized contents of the namespace suitable for
249
+ # writing to disk.
250
+ def create_missing_setting(key, initial_value = nil)
251
+ # Need to use `@settings` and `@mounts` here to stop recursive calls
252
+ return unless @mounts[key.to_s].nil?
253
+ return unless @settings[key.to_s].nil?
254
+ @settings[key.to_s] = PDK::Config::Setting.new(key.to_s, self, initial_value)
221
255
  end
222
256
 
223
257
  # Set the value of the named key.
@@ -228,40 +262,32 @@ module PDK
228
262
  # @param key [String,Symbol] the name of the configuration value.
229
263
  # @param value [Object] the value of the configuration value.
230
264
  def set_volatile_value(key, value)
231
- @values[key.to_s].validate!([name, key.to_s].join('.'), value) if @values.key?(key.to_s)
232
-
233
- data[key.to_s] = value
265
+ # Need to use `settings` here to force the backing file to be loaded
266
+ return create_missing_setting(key, value) if settings[key.to_s].nil?
267
+ # Need to use `@settings` here to stop recursive calls from []=
268
+ @settings[key.to_s].value = value
234
269
  end
235
270
 
236
- # Read the file associated with the namespace.
271
+ # Helper method to read files.
237
272
  #
238
273
  # @raise [PDK::Config::LoadError] if the file is removed during read.
239
274
  # @raise [PDK::Config::LoadError] if the user doesn't have the
240
275
  # permissions needed to read the file.
241
276
  # @return [String,nil] the contents of the file or nil if the file does
242
277
  # not exist.
243
- def load_data
244
- return if file.nil?
245
- return unless PDK::Util::Filesystem.file?(file)
278
+ def load_data(filename)
279
+ return if filename.nil?
280
+ return unless PDK::Util::Filesystem.file?(filename)
246
281
 
247
282
  PDK::Util::Filesystem.read_file(file)
248
283
  rescue Errno::ENOENT => e
249
284
  raise PDK::Config::LoadError, e.message
250
285
  rescue Errno::EACCES
251
286
  raise PDK::Config::LoadError, _('Unable to open %{file} for reading') % {
252
- file: file,
287
+ file: filename,
253
288
  }
254
289
  end
255
290
 
256
- # @abstract Subclass and override {#save_data} to implement generating
257
- # logic for a particular config file format.
258
- #
259
- # @param data [Hash{String => Object}] the data stored in the namespace
260
- #
261
- # @return [String] the serialized contents of the namespace suitable for
262
- # writing to disk.
263
- def serialize_data(_data); end
264
-
265
291
  # Persist the contents of the namespace to disk.
266
292
  #
267
293
  # Directories will be automatically created and the contents of the
@@ -289,35 +315,17 @@ module PDK
289
315
 
290
316
  # Memoised accessor for the loaded data.
291
317
  #
292
- # @return [Hash<String => Object>] the contents of the namespace.
293
- def data
294
- # It's possible for parse_data to return nil, so just return an empty hash
295
- @data ||= parse_data(load_data, file).tap do |h|
296
- h.default_proc = default_config_value unless h.nil?
297
- end || {}
298
- end
299
-
300
- # The default behaviour of the namespace when the requested value does
301
- # not exist.
302
- #
303
- # If the value has been pre-configured with {#value} to have a default
304
- # value, resolve the default value and set it in the namespace and optionally
305
- # save the new default.
306
- # Otherwise, set the value to a new Hash to allow for arbitrary level of nested values.
307
- #
308
- # @return [Proc] suitable for use by {Hash#default_proc}.
309
- def default_config_value
310
- ->(hash, key) do
311
- if @values.key?(key) && @values[key].default?
312
- set_volatile_value(key, @values[key].default)
313
- save_data if @persistent_defaults
314
- hash[key]
315
- else
316
- hash[key] = {}.tap do |h|
317
- h.default_proc = default_config_value
318
- end
319
- end
318
+ # @return [Hash<String => PDK::Config::Setting>] the contents of the namespace.
319
+ def settings
320
+ return @settings if @loaded_from_file
321
+ @loaded_from_file = true
322
+ return @settings if file.nil?
323
+ parse_file(file) do |key, parsed_setting|
324
+ # Create a settings chain if a setting already exists
325
+ parsed_setting.previous_setting = @settings[key] unless @settings[key].nil?
326
+ @settings[key] = parsed_setting
320
327
  end
328
+ @settings
321
329
  end
322
330
  end
323
331
  end