pdk 1.9.0 → 3.2.0

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