pdk 1.13.0 → 1.14.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 (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