pdk-akerl 1.9.1.1 → 1.14.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +180 -0
  3. data/README.md +43 -4
  4. data/lib/pdk.rb +4 -2
  5. data/lib/pdk/analytics.rb +44 -0
  6. data/lib/pdk/analytics/client/google_analytics.rb +141 -0
  7. data/lib/pdk/analytics/client/noop.rb +23 -0
  8. data/lib/pdk/analytics/util.rb +17 -0
  9. data/lib/pdk/answer_file.rb +4 -1
  10. data/lib/pdk/cli.rb +50 -3
  11. data/lib/pdk/cli/build.rb +10 -4
  12. data/lib/pdk/cli/bundle.rb +10 -8
  13. data/lib/pdk/cli/config.rb +20 -0
  14. data/lib/pdk/cli/config/get.rb +24 -0
  15. data/lib/pdk/cli/console.rb +148 -0
  16. data/lib/pdk/cli/convert.rb +7 -2
  17. data/lib/pdk/cli/exec.rb +22 -190
  18. data/lib/pdk/cli/exec/command.rb +238 -0
  19. data/lib/pdk/cli/exec/interactive_command.rb +114 -0
  20. data/lib/pdk/cli/exec_group.rb +6 -6
  21. data/lib/pdk/cli/module/build.rb +0 -2
  22. data/lib/pdk/cli/module/generate.rb +4 -2
  23. data/lib/pdk/cli/new.rb +2 -0
  24. data/lib/pdk/cli/new/class.rb +2 -2
  25. data/lib/pdk/cli/new/defined_type.rb +4 -2
  26. data/lib/pdk/cli/new/module.rb +5 -0
  27. data/lib/pdk/cli/new/provider.rb +4 -2
  28. data/lib/pdk/cli/new/task.rb +4 -1
  29. data/lib/pdk/cli/new/test.rb +53 -0
  30. data/lib/pdk/cli/new/transport.rb +27 -0
  31. data/lib/pdk/cli/test.rb +0 -1
  32. data/lib/pdk/cli/test/unit.rb +18 -13
  33. data/lib/pdk/cli/update.rb +25 -3
  34. data/lib/pdk/cli/util.rb +111 -14
  35. data/lib/pdk/cli/util/interview.rb +10 -2
  36. data/lib/pdk/cli/util/option_validator.rb +4 -0
  37. data/lib/pdk/cli/util/spinner.rb +13 -0
  38. data/lib/pdk/cli/validate.rb +16 -5
  39. data/lib/pdk/config.rb +121 -0
  40. data/lib/pdk/config/analytics_schema.json +26 -0
  41. data/lib/pdk/config/errors.rb +5 -0
  42. data/lib/pdk/config/json.rb +34 -0
  43. data/lib/pdk/config/json_schema_namespace.rb +143 -0
  44. data/lib/pdk/config/json_schema_setting.rb +53 -0
  45. data/lib/pdk/config/json_with_schema.rb +50 -0
  46. data/lib/pdk/config/namespace.rb +332 -0
  47. data/lib/pdk/config/setting.rb +132 -0
  48. data/lib/pdk/config/yaml.rb +43 -0
  49. data/lib/pdk/config/yaml_with_schema.rb +59 -0
  50. data/lib/pdk/generate.rb +10 -3
  51. data/lib/pdk/generate/defined_type.rb +1 -0
  52. data/lib/pdk/generate/module.rb +62 -35
  53. data/lib/pdk/generate/provider.rb +0 -5
  54. data/lib/pdk/generate/puppet_class.rb +1 -0
  55. data/lib/pdk/generate/puppet_object.rb +88 -41
  56. data/lib/pdk/generate/transport.rb +87 -0
  57. data/lib/pdk/logger.rb +21 -1
  58. data/lib/pdk/module.rb +2 -2
  59. data/lib/pdk/module/build.rb +103 -10
  60. data/lib/pdk/module/convert.rb +85 -19
  61. data/lib/pdk/module/metadata.rb +17 -12
  62. data/lib/pdk/module/templatedir.rb +108 -40
  63. data/lib/pdk/module/update.rb +27 -15
  64. data/lib/pdk/module/update_manager.rb +23 -15
  65. data/lib/pdk/report.rb +4 -3
  66. data/lib/pdk/report/event.rb +8 -6
  67. data/lib/pdk/template_file.rb +1 -1
  68. data/lib/pdk/tests/unit.rb +48 -21
  69. data/lib/pdk/util.rb +29 -63
  70. data/lib/pdk/util/bundler.rb +19 -15
  71. data/lib/pdk/util/filesystem.rb +64 -1
  72. data/lib/pdk/util/git.rb +52 -1
  73. data/lib/pdk/util/puppet_strings.rb +123 -0
  74. data/lib/pdk/util/puppet_version.rb +27 -12
  75. data/lib/pdk/util/ruby_version.rb +30 -7
  76. data/lib/pdk/util/template_uri.rb +281 -0
  77. data/lib/pdk/util/vendored_file.rb +28 -24
  78. data/lib/pdk/util/version.rb +7 -8
  79. data/lib/pdk/util/windows.rb +1 -0
  80. data/lib/pdk/util/windows/api_types.rb +0 -7
  81. data/lib/pdk/util/windows/file.rb +1 -1
  82. data/lib/pdk/util/windows/string.rb +1 -1
  83. data/lib/pdk/validate/base_validator.rb +12 -14
  84. data/lib/pdk/validate/metadata/metadata_json_lint.rb +0 -4
  85. data/lib/pdk/validate/metadata/metadata_syntax.rb +5 -3
  86. data/lib/pdk/validate/metadata_validator.rb +0 -2
  87. data/lib/pdk/validate/puppet/puppet_epp.rb +137 -0
  88. data/lib/pdk/validate/puppet/puppet_lint.rb +0 -3
  89. data/lib/pdk/validate/puppet/puppet_syntax.rb +5 -5
  90. data/lib/pdk/validate/puppet_validator.rb +2 -3
  91. data/lib/pdk/validate/ruby/rubocop.rb +1 -6
  92. data/lib/pdk/validate/ruby_validator.rb +0 -2
  93. data/lib/pdk/validate/tasks/metadata_lint.rb +9 -5
  94. data/lib/pdk/validate/tasks/name.rb +5 -3
  95. data/lib/pdk/validate/tasks_validator.rb +0 -2
  96. data/lib/pdk/validate/yaml/syntax.rb +6 -4
  97. data/lib/pdk/validate/yaml_validator.rb +0 -2
  98. data/lib/pdk/version.rb +1 -1
  99. data/locales/pdk.pot +634 -307
  100. metadata +100 -45
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ module PDK
2
+ class Config
3
+ class LoadError < StandardError; end
4
+ end
5
+ end
@@ -0,0 +1,34 @@
1
+ require 'pdk/config/namespace'
2
+
3
+ module PDK
4
+ class Config
5
+ class JSON < Namespace
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?
13
+
14
+ require 'json'
15
+
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) }
20
+ rescue ::JSON::ParserError => e
21
+ raise PDK::Config::LoadError, e.message
22
+ end
23
+
24
+ # Serializes object data into a JSON string.
25
+ #
26
+ # @see PDK::Config::Namespace.serialize_data
27
+ def serialize_data(data)
28
+ require 'json'
29
+
30
+ ::JSON.pretty_generate(data)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -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
@@ -0,0 +1,332 @@
1
+ module PDK
2
+ class Config
3
+ class Namespace
4
+ # @param value [String] the new name of this namespace.
5
+ attr_writer :name
6
+
7
+ # @return [String] the path to the file associated with the contents of
8
+ # this namespace.
9
+ attr_reader :file
10
+
11
+ # @return [self] the parent namespace of this namespace.
12
+ attr_accessor :parent
13
+
14
+ # Initialises the PDK::Config::Namespace object.
15
+ #
16
+ # @param name [String] the name of the namespace (defaults to nil).
17
+ # @param params [Hash{Symbol => Object}] keyword parameters for the
18
+ # method.
19
+ # @option params [String] :file the path to the file associated with the
20
+ # contents of the namespace (defaults to nil).
21
+ # @option params [self] :parent the parent {self} that this namespace is
22
+ # a child of (defaults to nil).
23
+ # @option params [self] :persistent_defaults whether default values should be persisted
24
+ # to disk when evaluated. By default they are not persisted to disk. This is typically
25
+ # used for settings which a randomly generated, instead of being deterministic, e.g. analytics user-id
26
+ # @param block [Proc] a block that is evaluated within the new instance.
27
+ def initialize(name = nil, file: nil, parent: nil, persistent_defaults: false, &block)
28
+ @file = File.expand_path(file) unless file.nil?
29
+ @settings = {}
30
+ @name = name.to_s
31
+ @parent = parent
32
+ @persistent_defaults = persistent_defaults
33
+ @mounts = {}
34
+ @loaded_from_file = false
35
+
36
+ instance_eval(&block) if block_given?
37
+ end
38
+
39
+ # Pre-configure a value in the namespace.
40
+ #
41
+ # Allows you to specify validators and a default value for value in the
42
+ # namespace (see PDK::Config::Value#initialize).
43
+ #
44
+ # @param key [String,Symbol] the name of the value.
45
+ # @param block [Proc] a block that is evaluated within the new [self].
46
+ #
47
+ # @return [nil]
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?
51
+ end
52
+
53
+ # Mount a provided [self] (or subclass) into the namespace.
54
+ #
55
+ # @param key [String,Symbol] the name of the namespace to be mounted.
56
+ # @param obj [self] the namespace to be mounted.
57
+ # @param block [Proc] a block to be evaluated within the instance of the
58
+ # newly mounted namespace.
59
+ #
60
+ # @raise [ArgumentError] if the object to be mounted is not a {self} or
61
+ # subclass thereof.
62
+ #
63
+ # @return [self] the mounted namespace.
64
+ def mount(key, obj, &block)
65
+ raise ArgumentError, _('Only PDK::Config::Namespace objects can be mounted into a namespace') unless obj.is_a?(PDK::Config::Namespace)
66
+ obj.parent = self
67
+ obj.name = key.to_s
68
+ obj.instance_eval(&block) if block_given?
69
+ @mounts[key.to_s] = obj
70
+ end
71
+
72
+ # Create and mount a new child namespace.
73
+ #
74
+ # @param name [String,Symbol] the name of the new namespace.
75
+ # @param block [Proc]
76
+ def namespace(name, &block)
77
+ mount(name, PDK::Config::Namespace.new, &block)
78
+ end
79
+
80
+ # Get the value of the named key.
81
+ #
82
+ # If there is a value for that key, return it. If not, follow the logic
83
+ # described in {#default_config_value} to determine the default value to
84
+ # return.
85
+ #
86
+ # @note Unlike a Ruby Hash, this will not return `nil` in the event that
87
+ # the key does not exist (see #fetch).
88
+ #
89
+ # @param key [String,Symbol] the name of the value to retrieve.
90
+ #
91
+ # @return [Object] the requested value.
92
+ def [](key)
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
104
+ end
105
+
106
+ # Get the value of the named key or the provided default value if not
107
+ # present. Note that this does not trigger persistent defaults
108
+ #
109
+ # This differs from {#[]} in an important way in that it allows you to
110
+ # return a default value, which is not possible using `[] || default` as
111
+ # non-existent values when accessed normally via {#[]} will be defaulted
112
+ # to a new Hash.
113
+ #
114
+ # @param key [String,Symbol] the name of the value to fetch.
115
+ # @param default_value [Object] the value to return if the namespace does
116
+ # not contain the requested value.
117
+ #
118
+ # @return [Object] the requested value.
119
+ def fetch(key, 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
126
+ end
127
+
128
+ # After the value has been set in memory, the value will then be
129
+ # persisted to disk.
130
+ #
131
+ # @param key [String,Symbol] the name of the configuration value.
132
+ # @param value [Object] the value of the configuration value.
133
+ #
134
+ # @return [nil]
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?
138
+ set_volatile_value(key, value)
139
+ # Persist the change
140
+ save_data
141
+ end
142
+
143
+ # Convert the namespace into a Hash of values, suitable for serialising
144
+ # and persisting to disk.
145
+ #
146
+ # Child namespaces that are associated with their own files are excluded
147
+ # from the Hash (as their values will be persisted to their own files)
148
+ # and nil values are removed from the Hash.
149
+ #
150
+ # @return [Hash{String => Object}] the values from the namespace that
151
+ # should be persisted to disk.
152
+ def to_h
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
158
+ end
159
+
160
+ # Resolves all filtered settings, including child namespaces, fully namespaced and filling in default values.
161
+ #
162
+ # @param filter [String] Only resolve setting names which match the filter. See #be_resolved? for matching rules
163
+ # @return [Hash{String => Object}] All resolved settings for example {'user.module_defaults.author' => 'johndoe'}
164
+ def resolve(filter = nil)
165
+ resolved = {}
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
171
+ end
172
+ end
173
+ # Resolve the mounts
174
+ @mounts.values.each { |mount| resolved.merge!(mount.resolve(filter)) }
175
+ resolved
176
+ end
177
+
178
+ # @return [Boolean] true if the namespace has a parent, otherwise false.
179
+ def child_namespace?
180
+ !parent.nil?
181
+ end
182
+
183
+ # Determines the fully qualified name of the namespace.
184
+ #
185
+ # If this is a child namespace, then fully qualified name for the
186
+ # namespace will be "<parent>.<child>".
187
+ #
188
+ # @return [String] the fully qualifed name of the namespace.
189
+ def name
190
+ child_namespace? ? [parent.name, @name].join('.') : @name
191
+ end
192
+
193
+ # Determines if the contents of the namespace should be included in the
194
+ # parent namespace when persisting to disk.
195
+ #
196
+ # If the namespace has been mounted into a parent namespace and is not
197
+ # associated with its own file on disk, then the values in the namespace
198
+ # should be included in the parent namespace when persisting to disk.
199
+ #
200
+ # @return [Boolean] true if the values should be included in the parent
201
+ # namespace.
202
+ def include_in_parent?
203
+ child_namespace? && file.nil?
204
+ end
205
+
206
+ private
207
+
208
+ # Determines whether a setting name should be resolved using the filter
209
+ # Returns true when filter is nil.
210
+ # Returns true if the filter is exactly the same name as the setting.
211
+ # Returns true if the name is a sub-key of the filter e.g.
212
+ # Given a filter of user.module_defaults, `user.module_defaults.author` will return true, but `user.analytics.disabled` will return false.
213
+ #
214
+ # @param name [String] The setting name to test.
215
+ # @param filter [String] The filter used to test on the name.
216
+ # @return [Boolean] Whether the name passes the filter.
217
+ def be_resolved?(name, filter = nil)
218
+ return true if filter.nil? # If we're not filtering, this value should always be resolved
219
+ return true if name == filter # If it's exactly the same name then it should be resolved
220
+ name.start_with?(filter + '.') # If name is a subkey of the filter then it should be resolved
221
+ end
222
+
223
+ # @abstract Subclass and override {#parse_file} to implement parsing logic
224
+ # for a particular config file format.
225
+ #
226
+ # @param data [String] The content of the file to be parsed.
227
+ # @param filename [String] The path to the file to be parsed.
228
+ #
229
+ # @yield [String, Object] the data to be loaded into the
230
+ # namespace.
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)
255
+ end
256
+
257
+ # Set the value of the named key.
258
+ #
259
+ # If the key has been pre-configured with {#value}, then the value of the
260
+ # key will be validated against any validators that have been configured.
261
+ #
262
+ # @param key [String,Symbol] the name of the configuration value.
263
+ # @param value [Object] the value of the configuration value.
264
+ def set_volatile_value(key, 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
269
+ end
270
+
271
+ # Helper method to read files.
272
+ #
273
+ # @raise [PDK::Config::LoadError] if the file is removed during read.
274
+ # @raise [PDK::Config::LoadError] if the user doesn't have the
275
+ # permissions needed to read the file.
276
+ # @return [String,nil] the contents of the file or nil if the file does
277
+ # not exist.
278
+ def load_data(filename)
279
+ return if filename.nil?
280
+ return unless PDK::Util::Filesystem.file?(filename)
281
+
282
+ PDK::Util::Filesystem.read_file(file)
283
+ rescue Errno::ENOENT => e
284
+ raise PDK::Config::LoadError, e.message
285
+ rescue Errno::EACCES
286
+ raise PDK::Config::LoadError, _('Unable to open %{file} for reading') % {
287
+ file: filename,
288
+ }
289
+ end
290
+
291
+ # Persist the contents of the namespace to disk.
292
+ #
293
+ # Directories will be automatically created and the contents of the
294
+ # namespace will be serialized automatically with {#serialize_data}.
295
+ #
296
+ # @raise [PDK::Config::LoadError] if one of the intermediary path components
297
+ # exist but is not a directory.
298
+ # @raise [PDK::Config::LoadError] if the user does not have the
299
+ # permissions needed to write the file.
300
+ #
301
+ # @return [nil]
302
+ def save_data
303
+ return if file.nil?
304
+
305
+ PDK::Util::Filesystem.mkdir_p(File.dirname(file))
306
+
307
+ PDK::Util::Filesystem.write_file(file, serialize_data(to_h))
308
+ rescue Errno::EACCES
309
+ raise PDK::Config::LoadError, _('Unable to open %{file} for writing') % {
310
+ file: file,
311
+ }
312
+ rescue SystemCallError => e
313
+ raise PDK::Config::LoadError, e.message
314
+ end
315
+
316
+ # Memoised accessor for the loaded data.
317
+ #
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
327
+ end
328
+ @settings
329
+ end
330
+ end
331
+ end
332
+ end