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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +180 -0
- data/README.md +43 -4
- data/lib/pdk.rb +4 -2
- data/lib/pdk/analytics.rb +44 -0
- data/lib/pdk/analytics/client/google_analytics.rb +141 -0
- data/lib/pdk/analytics/client/noop.rb +23 -0
- data/lib/pdk/analytics/util.rb +17 -0
- data/lib/pdk/answer_file.rb +4 -1
- data/lib/pdk/cli.rb +50 -3
- data/lib/pdk/cli/build.rb +10 -4
- data/lib/pdk/cli/bundle.rb +10 -8
- data/lib/pdk/cli/config.rb +20 -0
- data/lib/pdk/cli/config/get.rb +24 -0
- data/lib/pdk/cli/console.rb +148 -0
- data/lib/pdk/cli/convert.rb +7 -2
- data/lib/pdk/cli/exec.rb +22 -190
- data/lib/pdk/cli/exec/command.rb +238 -0
- data/lib/pdk/cli/exec/interactive_command.rb +114 -0
- data/lib/pdk/cli/exec_group.rb +6 -6
- data/lib/pdk/cli/module/build.rb +0 -2
- data/lib/pdk/cli/module/generate.rb +4 -2
- data/lib/pdk/cli/new.rb +2 -0
- data/lib/pdk/cli/new/class.rb +2 -2
- data/lib/pdk/cli/new/defined_type.rb +4 -2
- data/lib/pdk/cli/new/module.rb +5 -0
- data/lib/pdk/cli/new/provider.rb +4 -2
- data/lib/pdk/cli/new/task.rb +4 -1
- data/lib/pdk/cli/new/test.rb +53 -0
- data/lib/pdk/cli/new/transport.rb +27 -0
- data/lib/pdk/cli/test.rb +0 -1
- data/lib/pdk/cli/test/unit.rb +18 -13
- data/lib/pdk/cli/update.rb +25 -3
- data/lib/pdk/cli/util.rb +111 -14
- data/lib/pdk/cli/util/interview.rb +10 -2
- data/lib/pdk/cli/util/option_validator.rb +4 -0
- data/lib/pdk/cli/util/spinner.rb +13 -0
- data/lib/pdk/cli/validate.rb +16 -5
- data/lib/pdk/config.rb +121 -0
- data/lib/pdk/config/analytics_schema.json +26 -0
- data/lib/pdk/config/errors.rb +5 -0
- data/lib/pdk/config/json.rb +34 -0
- data/lib/pdk/config/json_schema_namespace.rb +143 -0
- data/lib/pdk/config/json_schema_setting.rb +53 -0
- data/lib/pdk/config/json_with_schema.rb +50 -0
- data/lib/pdk/config/namespace.rb +332 -0
- data/lib/pdk/config/setting.rb +132 -0
- data/lib/pdk/config/yaml.rb +43 -0
- data/lib/pdk/config/yaml_with_schema.rb +59 -0
- data/lib/pdk/generate.rb +10 -3
- data/lib/pdk/generate/defined_type.rb +1 -0
- data/lib/pdk/generate/module.rb +62 -35
- data/lib/pdk/generate/provider.rb +0 -5
- data/lib/pdk/generate/puppet_class.rb +1 -0
- data/lib/pdk/generate/puppet_object.rb +88 -41
- data/lib/pdk/generate/transport.rb +87 -0
- data/lib/pdk/logger.rb +21 -1
- data/lib/pdk/module.rb +2 -2
- data/lib/pdk/module/build.rb +103 -10
- data/lib/pdk/module/convert.rb +85 -19
- data/lib/pdk/module/metadata.rb +17 -12
- data/lib/pdk/module/templatedir.rb +108 -40
- data/lib/pdk/module/update.rb +27 -15
- data/lib/pdk/module/update_manager.rb +23 -15
- data/lib/pdk/report.rb +4 -3
- data/lib/pdk/report/event.rb +8 -6
- data/lib/pdk/template_file.rb +1 -1
- data/lib/pdk/tests/unit.rb +48 -21
- data/lib/pdk/util.rb +29 -63
- data/lib/pdk/util/bundler.rb +19 -15
- data/lib/pdk/util/filesystem.rb +64 -1
- data/lib/pdk/util/git.rb +52 -1
- data/lib/pdk/util/puppet_strings.rb +123 -0
- data/lib/pdk/util/puppet_version.rb +27 -12
- data/lib/pdk/util/ruby_version.rb +30 -7
- data/lib/pdk/util/template_uri.rb +281 -0
- data/lib/pdk/util/vendored_file.rb +28 -24
- data/lib/pdk/util/version.rb +7 -8
- data/lib/pdk/util/windows.rb +1 -0
- data/lib/pdk/util/windows/api_types.rb +0 -7
- data/lib/pdk/util/windows/file.rb +1 -1
- data/lib/pdk/util/windows/string.rb +1 -1
- data/lib/pdk/validate/base_validator.rb +12 -14
- data/lib/pdk/validate/metadata/metadata_json_lint.rb +0 -4
- data/lib/pdk/validate/metadata/metadata_syntax.rb +5 -3
- data/lib/pdk/validate/metadata_validator.rb +0 -2
- data/lib/pdk/validate/puppet/puppet_epp.rb +137 -0
- data/lib/pdk/validate/puppet/puppet_lint.rb +0 -3
- data/lib/pdk/validate/puppet/puppet_syntax.rb +5 -5
- data/lib/pdk/validate/puppet_validator.rb +2 -3
- data/lib/pdk/validate/ruby/rubocop.rb +1 -6
- data/lib/pdk/validate/ruby_validator.rb +0 -2
- data/lib/pdk/validate/tasks/metadata_lint.rb +9 -5
- data/lib/pdk/validate/tasks/name.rb +5 -3
- data/lib/pdk/validate/tasks_validator.rb +0 -2
- data/lib/pdk/validate/yaml/syntax.rb +6 -4
- data/lib/pdk/validate/yaml_validator.rb +0 -2
- data/lib/pdk/version.rb +1 -1
- data/locales/pdk.pot +634 -307
- 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,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
|