pdk 1.9.0 → 3.2.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 (163) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +744 -711
  3. data/README.md +23 -21
  4. data/lib/pdk/answer_file.rb +3 -112
  5. data/lib/pdk/bolt.rb +20 -0
  6. data/lib/pdk/cli/build.rb +51 -54
  7. data/lib/pdk/cli/bundle.rb +33 -29
  8. data/lib/pdk/cli/console.rb +148 -0
  9. data/lib/pdk/cli/convert.rb +46 -37
  10. data/lib/pdk/cli/env.rb +51 -0
  11. data/lib/pdk/cli/errors.rb +4 -3
  12. data/lib/pdk/cli/exec/command.rb +285 -0
  13. data/lib/pdk/cli/exec/interactive_command.rb +109 -0
  14. data/lib/pdk/cli/exec.rb +32 -201
  15. data/lib/pdk/cli/exec_group.rb +79 -43
  16. data/lib/pdk/cli/get/config.rb +26 -0
  17. data/lib/pdk/cli/get.rb +22 -0
  18. data/lib/pdk/cli/new/class.rb +20 -22
  19. data/lib/pdk/cli/new/defined_type.rb +21 -21
  20. data/lib/pdk/cli/new/fact.rb +27 -0
  21. data/lib/pdk/cli/new/function.rb +27 -0
  22. data/lib/pdk/cli/new/module.rb +40 -29
  23. data/lib/pdk/cli/new/provider.rb +18 -18
  24. data/lib/pdk/cli/new/task.rb +23 -22
  25. data/lib/pdk/cli/new/test.rb +52 -0
  26. data/lib/pdk/cli/new/transport.rb +27 -0
  27. data/lib/pdk/cli/new.rb +15 -9
  28. data/lib/pdk/cli/release/prep.rb +39 -0
  29. data/lib/pdk/cli/release/publish.rb +46 -0
  30. data/lib/pdk/cli/release.rb +185 -0
  31. data/lib/pdk/cli/remove/config.rb +83 -0
  32. data/lib/pdk/cli/remove.rb +22 -0
  33. data/lib/pdk/cli/set/config.rb +121 -0
  34. data/lib/pdk/cli/set.rb +22 -0
  35. data/lib/pdk/cli/test/unit.rb +71 -69
  36. data/lib/pdk/cli/test.rb +9 -8
  37. data/lib/pdk/cli/update.rb +38 -21
  38. data/lib/pdk/cli/util/command_redirector.rb +13 -3
  39. data/lib/pdk/cli/util/interview.rb +25 -9
  40. data/lib/pdk/cli/util/option_normalizer.rb +6 -6
  41. data/lib/pdk/cli/util/option_validator.rb +19 -9
  42. data/lib/pdk/cli/util/spinner.rb +13 -0
  43. data/lib/pdk/cli/util/update_manager_printer.rb +82 -0
  44. data/lib/pdk/cli/util.rb +105 -48
  45. data/lib/pdk/cli/validate.rb +96 -111
  46. data/lib/pdk/cli.rb +134 -87
  47. data/lib/pdk/config/errors.rb +5 -0
  48. data/lib/pdk/config/ini_file.rb +184 -0
  49. data/lib/pdk/config/ini_file_setting.rb +35 -0
  50. data/lib/pdk/config/json.rb +35 -0
  51. data/lib/pdk/config/json_schema_namespace.rb +137 -0
  52. data/lib/pdk/config/json_schema_setting.rb +51 -0
  53. data/lib/pdk/config/json_with_schema.rb +47 -0
  54. data/lib/pdk/config/namespace.rb +362 -0
  55. data/lib/pdk/config/setting.rb +134 -0
  56. data/lib/pdk/config/task_schema.json +116 -0
  57. data/lib/pdk/config/validator.rb +31 -0
  58. data/lib/pdk/config/yaml.rb +41 -0
  59. data/lib/pdk/config/yaml_with_schema.rb +51 -0
  60. data/lib/pdk/config.rb +304 -0
  61. data/lib/pdk/context/control_repo.rb +61 -0
  62. data/lib/pdk/context/module.rb +28 -0
  63. data/lib/pdk/context/none.rb +22 -0
  64. data/lib/pdk/context.rb +98 -0
  65. data/lib/pdk/control_repo.rb +89 -0
  66. data/lib/pdk/generate/defined_type.rb +27 -33
  67. data/lib/pdk/generate/fact.rb +26 -0
  68. data/lib/pdk/generate/function.rb +49 -0
  69. data/lib/pdk/generate/module.rb +160 -153
  70. data/lib/pdk/generate/provider.rb +16 -69
  71. data/lib/pdk/generate/puppet_class.rb +27 -32
  72. data/lib/pdk/generate/puppet_object.rb +100 -159
  73. data/lib/pdk/generate/task.rb +34 -51
  74. data/lib/pdk/generate/transport.rb +34 -0
  75. data/lib/pdk/generate.rb +21 -8
  76. data/lib/pdk/logger.rb +24 -6
  77. data/lib/pdk/module/build.rb +125 -37
  78. data/lib/pdk/module/convert.rb +146 -65
  79. data/lib/pdk/module/metadata.rb +72 -71
  80. data/lib/pdk/module/release.rb +255 -0
  81. data/lib/pdk/module/update.rb +48 -37
  82. data/lib/pdk/module/update_manager.rb +75 -39
  83. data/lib/pdk/module.rb +10 -2
  84. data/lib/pdk/monkey_patches.rb +268 -0
  85. data/lib/pdk/report/event.rb +36 -48
  86. data/lib/pdk/report.rb +35 -22
  87. data/lib/pdk/template/fetcher/git.rb +84 -0
  88. data/lib/pdk/template/fetcher/local.rb +29 -0
  89. data/lib/pdk/template/fetcher.rb +100 -0
  90. data/lib/pdk/template/renderer/v1/legacy_template_dir.rb +108 -0
  91. data/lib/pdk/template/renderer/v1/renderer.rb +131 -0
  92. data/lib/pdk/template/renderer/v1/template_file.rb +100 -0
  93. data/lib/pdk/template/renderer/v1.rb +25 -0
  94. data/lib/pdk/template/renderer.rb +97 -0
  95. data/lib/pdk/template/template_dir.rb +67 -0
  96. data/lib/pdk/template.rb +52 -0
  97. data/lib/pdk/tests/unit.rb +101 -51
  98. data/lib/pdk/util/bundler.rb +44 -42
  99. data/lib/pdk/util/changelog_generator.rb +138 -0
  100. data/lib/pdk/util/env.rb +48 -0
  101. data/lib/pdk/util/filesystem.rb +139 -2
  102. data/lib/pdk/util/git.rb +108 -8
  103. data/lib/pdk/util/json_finder.rb +86 -0
  104. data/lib/pdk/util/puppet_strings.rb +125 -0
  105. data/lib/pdk/util/puppet_version.rb +71 -87
  106. data/lib/pdk/util/ruby_version.rb +49 -25
  107. data/lib/pdk/util/template_uri.rb +283 -0
  108. data/lib/pdk/util/vendored_file.rb +34 -42
  109. data/lib/pdk/util/version.rb +11 -10
  110. data/lib/pdk/util/windows/api_types.rb +74 -44
  111. data/lib/pdk/util/windows/file.rb +31 -27
  112. data/lib/pdk/util/windows/process.rb +74 -0
  113. data/lib/pdk/util/windows/string.rb +19 -12
  114. data/lib/pdk/util/windows.rb +2 -0
  115. data/lib/pdk/util.rb +111 -124
  116. data/lib/pdk/validate/control_repo/control_repo_validator_group.rb +23 -0
  117. data/lib/pdk/validate/control_repo/environment_conf_validator.rb +98 -0
  118. data/lib/pdk/validate/external_command_validator.rb +213 -0
  119. data/lib/pdk/validate/internal_ruby_validator.rb +101 -0
  120. data/lib/pdk/validate/invokable_validator.rb +238 -0
  121. data/lib/pdk/validate/metadata/metadata_json_lint_validator.rb +84 -0
  122. data/lib/pdk/validate/metadata/metadata_syntax_validator.rb +76 -0
  123. data/lib/pdk/validate/metadata/metadata_validator_group.rb +20 -0
  124. data/lib/pdk/validate/puppet/puppet_epp_validator.rb +131 -0
  125. data/lib/pdk/validate/puppet/puppet_lint_validator.rb +66 -0
  126. data/lib/pdk/validate/puppet/puppet_plan_syntax_validator.rb +38 -0
  127. data/lib/pdk/validate/puppet/puppet_syntax_validator.rb +135 -0
  128. data/lib/pdk/validate/puppet/puppet_validator_group.rb +22 -0
  129. data/lib/pdk/validate/ruby/ruby_rubocop_validator.rb +79 -0
  130. data/lib/pdk/validate/ruby/ruby_validator_group.rb +19 -0
  131. data/lib/pdk/validate/tasks/tasks_metadata_lint_validator.rb +83 -0
  132. data/lib/pdk/validate/tasks/tasks_name_validator.rb +45 -0
  133. data/lib/pdk/validate/tasks/tasks_validator_group.rb +20 -0
  134. data/lib/pdk/validate/validator.rb +120 -0
  135. data/lib/pdk/validate/validator_group.rb +107 -0
  136. data/lib/pdk/validate/yaml/yaml_syntax_validator.rb +86 -0
  137. data/lib/pdk/validate/yaml/yaml_validator_group.rb +19 -0
  138. data/lib/pdk/validate.rb +86 -12
  139. data/lib/pdk/version.rb +2 -2
  140. data/lib/pdk.rb +60 -10
  141. metadata +138 -100
  142. data/lib/pdk/cli/module/build.rb +0 -14
  143. data/lib/pdk/cli/module/generate.rb +0 -45
  144. data/lib/pdk/cli/module.rb +0 -14
  145. data/lib/pdk/i18n.rb +0 -4
  146. data/lib/pdk/module/templatedir.rb +0 -321
  147. data/lib/pdk/template_file.rb +0 -95
  148. data/lib/pdk/validate/base_validator.rb +0 -215
  149. data/lib/pdk/validate/metadata/metadata_json_lint.rb +0 -86
  150. data/lib/pdk/validate/metadata/metadata_syntax.rb +0 -109
  151. data/lib/pdk/validate/metadata_validator.rb +0 -30
  152. data/lib/pdk/validate/puppet/puppet_lint.rb +0 -67
  153. data/lib/pdk/validate/puppet/puppet_syntax.rb +0 -112
  154. data/lib/pdk/validate/puppet_validator.rb +0 -30
  155. data/lib/pdk/validate/ruby/rubocop.rb +0 -77
  156. data/lib/pdk/validate/ruby_validator.rb +0 -29
  157. data/lib/pdk/validate/tasks/metadata_lint.rb +0 -126
  158. data/lib/pdk/validate/tasks/name.rb +0 -88
  159. data/lib/pdk/validate/tasks_validator.rb +0 -33
  160. data/lib/pdk/validate/yaml/syntax.rb +0 -109
  161. data/lib/pdk/validate/yaml_validator.rb +0 -31
  162. data/locales/config.yaml +0 -21
  163. data/locales/pdk.pot +0 -1291
@@ -0,0 +1,51 @@
1
+ require 'pdk'
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
+
12
+ super
13
+ end
14
+
15
+ # Verifies that the new setting value is valid by calling the JSON schema validator on
16
+ # a hash which includes the new setting
17
+ #
18
+ # @see PDK::Config::Setting.validate!
19
+ def validate!(value)
20
+ # Get the existing namespace data
21
+ new_document = namespace.to_h
22
+ # ... set the new value
23
+ new_document[@name] = value
24
+ begin
25
+ # ... add validate it
26
+ namespace.validate_document!(new_document)
27
+ rescue ::JSON::Schema::ValidationError => e
28
+ raise ArgumentError, format('%{key} %{message}', key: qualified_name, message: e.message)
29
+ end
30
+ end
31
+
32
+ # Evaluate the default setting, firstly from the JSON schema and then
33
+ # from any other default evaluators in the settings chain.
34
+ #
35
+ # @see PDK::Config::Setting.default
36
+ #
37
+ # @return [Object, nil] the result of evaluating the block given to
38
+ # {#default_to}, or `nil` if the setting has no default.
39
+ def default
40
+ # Return the default from the schema document if it exists
41
+ if namespace.schema_property_names.include?(@name)
42
+ prop_schema = namespace.schema['properties'][@name]
43
+ return prop_schema['default'] unless prop_schema['default'].nil?
44
+ end
45
+ # ... otherwise call the settings chain default
46
+ # and if that doesn't exist, just return nil
47
+ @previous_setting&.default
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,47 @@
1
+ require 'pdk'
2
+
3
+ module PDK
4
+ class Config
5
+ class JSONWithSchema < JSONSchemaNamespace
6
+ # Parses a JSON document with a schema.
7
+ #
8
+ # @see PDK::Config::Namespace.parse_file
9
+ def parse_file(filename)
10
+ raise unless block_given?
11
+
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, format('The configuration file %{filename} is not valid: %{message}', filename: filename, message: e.message)
24
+ end
25
+
26
+ schema_property_names.each do |key|
27
+ yield key, PDK::Config::JSONSchemaSetting.new(key, self, @raw_data[key])
28
+ end
29
+
30
+ # Remove all of the "known" settings from the schema and
31
+ # we're left with the settings that we don't manage.
32
+ self.unmanaged_settings = @raw_data.reject { |k, _| schema_property_names.include?(k) }
33
+ rescue ::JSON::ParserError => e
34
+ raise PDK::Config::LoadError, e.message
35
+ end
36
+
37
+ # Serializes object data into a JSON string.
38
+ #
39
+ # @see PDK::Config::Namespace.serialize_data
40
+ def serialize_data(data)
41
+ require 'json'
42
+
43
+ ::JSON.pretty_generate(data)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,362 @@
1
+ require 'pdk'
2
+
3
+ module PDK
4
+ class Config
5
+ class Namespace
6
+ # @param value [String] the new name of this namespace.
7
+ attr_writer :name
8
+
9
+ # @return [String] the path to the file associated with the contents of
10
+ # this namespace.
11
+ attr_reader :file
12
+
13
+ # @return [self] the parent namespace of this namespace.
14
+ attr_accessor :parent
15
+
16
+ # Initialises the PDK::Config::Namespace object.
17
+ #
18
+ # @param name [String] the name of the namespace (defaults to nil).
19
+ # @param params [Hash{Symbol => Object}] keyword parameters for the
20
+ # method.
21
+ # @option params [String] :file the path to the file associated with the
22
+ # contents of the namespace (defaults to nil).
23
+ # @option params [self] :parent the parent {self} that this namespace is
24
+ # a child of (defaults to nil).
25
+ # @option params [self] :persistent_defaults whether default values should be persisted
26
+ # to disk when evaluated. By default they are not persisted to disk. This is typically
27
+ # used for settings which a randomly generated, instead of being deterministic, e.g. module_defaults author
28
+ # @param block [Proc] a block that is evaluated within the new instance.
29
+ def initialize(name = nil, file: nil, parent: nil, persistent_defaults: false, &block)
30
+ @file = PDK::Util::Filesystem.expand_path(file) unless file.nil?
31
+ @settings = {}
32
+ @name = name.to_s
33
+ @parent = parent
34
+ @persistent_defaults = persistent_defaults
35
+ @mounts = {}
36
+ @loaded_from_file = false
37
+ @read_only = false
38
+
39
+ instance_eval(&block) if block
40
+ end
41
+
42
+ # Pre-configure a value in the namespace.
43
+ #
44
+ # Allows you to specify validators and a default value for value in the
45
+ # namespace (see PDK::Config::Value#initialize).
46
+ #
47
+ # @param key [String,Symbol] the name of the value.
48
+ # @param block [Proc] a block that is evaluated within the new [self].
49
+ #
50
+ # @return [nil]
51
+ def setting(key, &block)
52
+ @settings[key.to_s] ||= default_setting_class.new(key.to_s, self)
53
+ @settings[key.to_s].instance_eval(&block) if block
54
+ end
55
+
56
+ # Mount a provided [self] (or subclass) into the namespace.
57
+ #
58
+ # @param key [String,Symbol] the name of the namespace to be mounted.
59
+ # @param obj [self] the namespace to be mounted.
60
+ # @param block [Proc] a block to be evaluated within the instance of the
61
+ # newly mounted namespace.
62
+ #
63
+ # @raise [ArgumentError] if the object to be mounted is not a {self} or
64
+ # subclass thereof.
65
+ #
66
+ # @return [self] the mounted namespace.
67
+ def mount(key, obj, &block)
68
+ raise ArgumentError, 'Only PDK::Config::Namespace objects can be mounted into a namespace' unless obj.is_a?(PDK::Config::Namespace)
69
+
70
+ obj.parent = self
71
+ obj.name = key.to_s
72
+ obj.instance_eval(&block) if block
73
+ @mounts[key.to_s] = obj
74
+ end
75
+
76
+ # Create and mount a new child namespace.
77
+ #
78
+ # @param name [String,Symbol] the name of the new namespace.
79
+ # @param block [Proc]
80
+ def namespace(name, &block)
81
+ mount(name, PDK::Config::Namespace.new, &block)
82
+ end
83
+
84
+ # Get the value of the named key.
85
+ #
86
+ # If there is a value for that key, return it. If not, follow the logic
87
+ # described in {#default_config_value} to determine the default value to
88
+ # return.
89
+ #
90
+ # @note Unlike a Ruby Hash, this will not return `nil` in the event that
91
+ # the key does not exist (see #fetch).
92
+ #
93
+ # @param key [String,Symbol] the name of the value to retrieve.
94
+ #
95
+ # @return [Object] the requested value.
96
+ def [](key)
97
+ # Check if it's a mount first...
98
+ return @mounts[key.to_s] unless @mounts[key.to_s].nil?
99
+ # Check if it's a setting, otherwise nil
100
+ return nil if settings[key.to_s].nil?
101
+ return settings[key.to_s].value unless settings[key.to_s].value.nil?
102
+
103
+ # Duplicate arrays and hashes so that they are isolated from changes being made
104
+ default_value = PDK::Util.deep_duplicate(settings[key.to_s].default)
105
+ return default_value if default_value.nil? || !@persistent_defaults
106
+
107
+ # Persist the default value
108
+ settings[key.to_s].value = default_value
109
+ save_data
110
+ default_value
111
+ end
112
+
113
+ # Get the value of the named key or the provided default value if not
114
+ # present. Note that this does not trigger persistent defaults
115
+ #
116
+ # This differs from {#[]} in an important way in that it allows you to
117
+ # return a default value, which is not possible using `[] || default` as
118
+ # non-existent values when accessed normally via {#[]} will be defaulted
119
+ # to a new Hash.
120
+ #
121
+ # @param key [String,Symbol] the name of the value to fetch.
122
+ # @param default_value [Object] the value to return if the namespace does
123
+ # not contain the requested value.
124
+ #
125
+ # @return [Object] the requested value.
126
+ def fetch(key, default_value)
127
+ # Check if it's a mount first...
128
+ return @mounts[key.to_s] unless @mounts[key.to_s].nil?
129
+ # Check if it's a setting, otherwise default_value
130
+ return default_value if settings[key.to_s].nil?
131
+
132
+ # Check if has a value, otherwise default_value
133
+ settings[key.to_s].value.nil? ? default_value : settings[key.to_s].value
134
+ end
135
+
136
+ # After the value has been set in memory, the value will then be
137
+ # persisted to disk.
138
+ #
139
+ # @param key [String,Symbol] the name of the configuration value.
140
+ # @param value [Object] the value of the configuration value.
141
+ #
142
+ # @return [nil]
143
+ def []=(key, value)
144
+ # You can't set the value of a mount
145
+ raise ArgumentError, 'Namespace mounts can not be set a value' unless @mounts[key.to_s].nil?
146
+
147
+ set_volatile_value(key, value)
148
+ # Persist the change
149
+ save_data
150
+ end
151
+
152
+ # Convert the namespace into a Hash of values, suitable for serialising
153
+ # and persisting to disk.
154
+ #
155
+ # Child namespaces that are associated with their own files are excluded
156
+ # from the Hash (as their values will be persisted to their own files)
157
+ # and nil values are removed from the Hash.
158
+ #
159
+ # @return [Hash{String => Object}] the values from the namespace that
160
+ # should be persisted to disk.
161
+ def to_h
162
+ new_hash = {}
163
+ settings.each_pair { |k, v| new_hash[k] = v.value }
164
+ @mounts.each_pair { |k, mount_point| new_hash[k] = mount_point.to_h if mount_point.include_in_parent? }
165
+ new_hash.delete_if { |_k, v| v.nil? }
166
+ new_hash
167
+ end
168
+
169
+ # Resolves all filtered settings, including child namespaces, fully namespaced and filling in default values.
170
+ #
171
+ # @param filter [String] Only resolve setting names which match the filter. See #be_resolved? for matching rules
172
+ # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'}
173
+ def resolve(filter = nil)
174
+ resolved = {}
175
+ # Resolve the settings
176
+ settings.each_value do |setting|
177
+ setting_name = setting.qualified_name
178
+ if be_resolved?(setting_name, filter)
179
+ resolved[setting_name] = setting.value.nil? ? setting.default : setting.value
180
+ end
181
+ end
182
+ # Resolve the mounts
183
+ @mounts.each_value { |mount| resolved.merge!(mount.resolve(filter)) }
184
+ resolved
185
+ end
186
+
187
+ # @return [Boolean] true if the namespace has a parent, otherwise false.
188
+ def child_namespace?
189
+ !parent.nil?
190
+ end
191
+
192
+ # Determines the fully qualified name of the namespace.
193
+ #
194
+ # If this is a child namespace, then fully qualified name for the
195
+ # namespace will be "<parent>.<child>".
196
+ #
197
+ # @return [String] the fully qualifed name of the namespace.
198
+ def name
199
+ child_namespace? ? [parent.name, @name].join('.') : @name
200
+ end
201
+
202
+ # Determines if the contents of the namespace should be included in the
203
+ # parent namespace when persisting to disk.
204
+ #
205
+ # If the namespace has been mounted into a parent namespace and is not
206
+ # associated with its own file on disk, then the values in the namespace
207
+ # should be included in the parent namespace when persisting to disk.
208
+ #
209
+ # @return [Boolean] true if the values should be included in the parent
210
+ # namespace.
211
+ def include_in_parent?
212
+ child_namespace? && file.nil?
213
+ end
214
+
215
+ # Disables the namespace, and child namespaces, from writing changes to disk.
216
+ # Typically this is only needed for unit testing.
217
+ # @api private
218
+ def read_only!
219
+ @read_only = true
220
+ # pass the read_only! method as a block to the each_value method. This means that
221
+ # for each value in the @mounts hash, the read_only! method will be called on that value.
222
+ @mounts.each_value(&:read_only!)
223
+ end
224
+
225
+ private
226
+
227
+ # Returns the object class to create settings with. Subclasses may override this to use specific setting classes
228
+ #
229
+ # @return [Class[PDK::Config::Setting]]
230
+ #
231
+ # @abstract
232
+ # @private
233
+ def default_setting_class
234
+ PDK::Config::Setting
235
+ end
236
+
237
+ # Determines whether a setting name should be resolved using the filter
238
+ # Returns true when filter is nil.
239
+ # Returns true if the filter is exactly the same name as the setting.
240
+ # Returns true if the name is a sub-key of the filter e.g.
241
+ # Given a filter of user.module_defaults, `user.module_defaults.author` will return true, but `user.pdk_feature_flags.requested` will return false.
242
+ #
243
+ # @param name [String] The setting name to test.
244
+ # @param filter [String] The filter used to test on the name.
245
+ # @return [Boolean] Whether the name passes the filter.
246
+ def be_resolved?(name, filter = nil)
247
+ return true if filter.nil? # If we're not filtering, this value should always be resolved
248
+ return true if name == filter # If it's exactly the same name then it should be resolved
249
+
250
+ name.start_with?("#{filter}.") # If name is a subkey of the filter then it should be resolved
251
+ end
252
+
253
+ # @abstract Subclass and override {#parse_file} to implement parsing logic
254
+ # for a particular config file format.
255
+ #
256
+ # @param data [String] The content of the file to be parsed.
257
+ # @param filename [String] The path to the file to be parsed.
258
+ #
259
+ # @yield [String, Object] the data to be loaded into the
260
+ # namespace.
261
+ def parse_file(_filename); end
262
+
263
+ # @abstract Subclass and override {#serialize_data} to implement generating
264
+ # logic for a particular config file format.
265
+ #
266
+ # @param data [Hash{String => Object}] the data stored in the namespace
267
+ #
268
+ # @return [String] the serialized contents of the namespace suitable for
269
+ # writing to disk.
270
+ def serialize_data(_data); end
271
+
272
+ # @abstract Subclass and override {#create_missing_setting} to implement logic
273
+ # when a setting is dynamically created, for example when attempting to
274
+ # set the value of an unknown setting
275
+ #
276
+ # @param data [Hash{String => Object}] the data stored in the namespace
277
+ #
278
+ # @return [String] the serialized contents of the namespace suitable for
279
+ # writing to disk.
280
+ def create_missing_setting(key, initial_value = nil)
281
+ # Need to use `@settings` and `@mounts` here to stop recursive calls
282
+ return unless @mounts[key.to_s].nil?
283
+ return unless @settings[key.to_s].nil?
284
+
285
+ @settings[key.to_s] = default_setting_class.new(key.to_s, self, initial_value)
286
+ end
287
+
288
+ # Set the value of the named key.
289
+ #
290
+ # If the key has been pre-configured with {#value}, then the value of the
291
+ # key will be validated against any validators that have been configured.
292
+ #
293
+ # @param key [String,Symbol] the name of the configuration value.
294
+ # @param value [Object] the value of the configuration value.
295
+ def set_volatile_value(key, value)
296
+ # Need to use `settings` here to force the backing file to be loaded
297
+ return create_missing_setting(key, value) if settings[key.to_s].nil?
298
+
299
+ # Need to use `@settings` here to stop recursive calls from []=
300
+ @settings[key.to_s].value = value
301
+ end
302
+
303
+ # Helper method to read files.
304
+ #
305
+ # @raise [PDK::Config::LoadError] if the file is removed during read.
306
+ # @raise [PDK::Config::LoadError] if the user doesn't have the
307
+ # permissions needed to read the file.
308
+ # @return [String,nil] the contents of the file or nil if the file does
309
+ # not exist.
310
+ def load_data(filename)
311
+ return if filename.nil?
312
+ return unless PDK::Util::Filesystem.file?(filename)
313
+
314
+ PDK::Util::Filesystem.read_file(filename)
315
+ rescue Errno::ENOENT => e
316
+ raise PDK::Config::LoadError, e.message
317
+ rescue Errno::EACCES
318
+ raise PDK::Config::LoadError, format('Unable to open %{file} for reading', file: filename)
319
+ end
320
+
321
+ # Persist the contents of the namespace to disk.
322
+ #
323
+ # Directories will be automatically created and the contents of the
324
+ # namespace will be serialized automatically with {#serialize_data}.
325
+ #
326
+ # @raise [PDK::Config::LoadError] if one of the intermediary path components
327
+ # exist but is not a directory.
328
+ # @raise [PDK::Config::LoadError] if the user does not have the
329
+ # permissions needed to write the file.
330
+ #
331
+ # @return [nil]
332
+ def save_data
333
+ return if file.nil? || @read_only
334
+
335
+ PDK::Util::Filesystem.mkdir_p(File.dirname(file))
336
+
337
+ PDK::Util::Filesystem.write_file(file, serialize_data(to_h))
338
+ rescue Errno::EACCES
339
+ raise PDK::Config::LoadError, format('Unable to open %{file} for writing', file: file)
340
+ rescue SystemCallError => e
341
+ raise PDK::Config::LoadError, e.message
342
+ end
343
+
344
+ # Memoised accessor for the loaded data.
345
+ #
346
+ # @return [Hash<String => PDK::Config::Setting>] the contents of the namespace.
347
+ def settings
348
+ return @settings if @loaded_from_file
349
+
350
+ @loaded_from_file = true
351
+ return @settings if file.nil?
352
+
353
+ parse_file(file) do |key, parsed_setting|
354
+ # Create a settings chain if a setting already exists
355
+ parsed_setting.previous_setting = @settings[key] unless @settings[key].nil?
356
+ @settings[key] = parsed_setting
357
+ end
358
+ @settings
359
+ end
360
+ end
361
+ end
362
+ end
@@ -0,0 +1,134 @@
1
+ require 'pdk'
2
+
3
+ module PDK
4
+ class Config
5
+ # A class for describing the setting of a {PDK::Config} setting.
6
+ #
7
+ # Generally, this is never instantiated manually, but is instead
8
+ # instantiated by passing a block to {PDK::Config::Namespace#setting}.
9
+ #
10
+ # @example
11
+ #
12
+ # PDK::Config::Namespace.new('module_defaults') do
13
+ # setting :author do
14
+ # validate PDK::Config::Validator.string
15
+ # default_to { false }
16
+ # end
17
+ # end
18
+ class Setting
19
+ attr_reader :namespace
20
+
21
+ # It is possible to have multiple setting definitions for the same setting; for example, defining a default value with a lambda, but the
22
+ # the validation is within a JSON schema document. These are expressed as two settings objects, and uses a single linked list to join them
23
+ # together:
24
+ #
25
+ # (PDK::Config::JSONSchemaSetting) --previous_setting--> (PDK::Config::Setting)
26
+ #
27
+ # So in the example above, calling `default` the on the first object in the list will:
28
+ # 1. Look at `default` on PDK::Config::JSONSchemaSetting
29
+ # 2. If a default could not be found then it calls `default` on previous_setting
30
+ # 3. If a default could not be found then it calls `default` on previous_setting.previous_setting
31
+ # 4. and so on down the linked list (chain) of settings
32
+ attr_writer :previous_setting
33
+
34
+ # Initialises an empty setting definition.
35
+ #
36
+ # @param name [String,Symbol] the name of the setting.
37
+ # @param namespace [PDK::Config::Namespace] The namespace this setting belongs to
38
+ def initialize(name, namespace, initial_value = nil)
39
+ @name = name.to_s
40
+ @validators = []
41
+ @namespace = namespace
42
+ @value = initial_value
43
+ end
44
+
45
+ def qualified_name
46
+ [namespace.name, @name].join('.')
47
+ end
48
+
49
+ def value
50
+ # Duplicate arrays and hashes so that they are isolated from changes being made
51
+ PDK::Util.deep_duplicate(@value)
52
+ end
53
+
54
+ def value=(obj)
55
+ validate!(obj)
56
+ @value = obj
57
+ end
58
+
59
+ def to_s
60
+ @value.to_s
61
+ end
62
+
63
+ # Assign a validator to the setting. Subclasses should not override this method.
64
+ #
65
+ # @param validator [Hash{Symbol => [Proc,String]}]
66
+ # @option validator [Proc] :proc a lambda that takes the setting to be
67
+ # validated as the argument and returns `true` if the setting is valid.
68
+ # @option validator [String] :message a description of what the validator
69
+ # is testing for, that is displayed to the user as part of the error
70
+ # message for invalid settings.
71
+ #
72
+ # @raise [ArgumentError] if not passed a Hash.
73
+ # @raise [ArgumentError] if the Hash doesn't have a `:proc` key that
74
+ # contains a Proc.
75
+ # @raise [ArgumentError] if the Hash doesn't have a `:message` key that
76
+ # contains a String.
77
+ #
78
+ # @return [nil]
79
+ def validate(validator)
80
+ raise ArgumentError, '`validator` must be a Hash' unless validator.is_a?(Hash)
81
+ raise ArgumentError, 'the :proc key must contain a Proc' unless validator.key?(:proc) && validator[:proc].is_a?(Proc)
82
+ raise ArgumentError, 'the :message key must contain a String' unless validator.key?(:message) && validator[:message].is_a?(String)
83
+
84
+ @validators << validator
85
+ end
86
+
87
+ # Validate a setting against the assigned validators.
88
+ #
89
+ # @param setting [Object] the setting being validated.
90
+ #
91
+ # @raise [ArgumentError] if any of the assigned validators fail to
92
+ # validate the setting.
93
+ #
94
+ # @return [nil]
95
+ def validate!(value)
96
+ @validators.each do |validator|
97
+ next if validator[:proc].call(value)
98
+
99
+ raise ArgumentError, format('%{key} %{message}', key: qualified_name, message: validator[:message])
100
+ end
101
+ end
102
+
103
+ # Assign a default value proc for the setting. Subclasses should not override this method.
104
+ #
105
+ # @param block [Proc] a block that is lazy evaluated when necessary in
106
+ # order to determine the default setting.
107
+ #
108
+ # @return [nil]
109
+ def default_to(&block)
110
+ raise ArgumentError, 'must be passed a block' unless block
111
+
112
+ @default_to = block
113
+ end
114
+
115
+ # Evaluate the default setting.
116
+ #
117
+ # @return [Object,nil] the result of evaluating the block given to
118
+ # {#default_to}, or `nil` if the setting has no default.
119
+ def default
120
+ return @default_to.call if default_block?
121
+
122
+ # If there is a previous setting in the chain, use its default
123
+ @previous_setting&.default
124
+ end
125
+
126
+ private
127
+
128
+ # @return [Boolean] true if the setting has a default setting block. Subclasses should not override this method.
129
+ def default_block?
130
+ !@default_to.nil?
131
+ end
132
+ end
133
+ end
134
+ end