aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorPatrick Spek <p.spek@tyil.nl>2020-07-04 13:46:25 +0200
committerPatrick Spek <p.spek@tyil.nl>2023-07-25 02:17:24 +0200
commitbb450bd96b02b48ed8b277fa0a6ca5c97037a680 (patch)
tree974bf72d443dbf0d1bffea29793becee806b43bc /lib
parent86d3fe57ceb163d9ff31d131568f4a9caca8bbc5 (diff)
Overhaul Config to 3.0.0
Diffstat (limited to 'lib')
-rw-r--r--lib/Config.pm6654
-rw-r--r--lib/Config/Parser.pm629
-rw-r--r--lib/Config/Parser/NULL.pm645
-rw-r--r--lib/X/Config/AbstractParser.rakumod36
-rw-r--r--lib/X/Config/FileNoExtension.rakumod38
-rw-r--r--lib/X/Config/FileNotFound.rakumod38
-rw-r--r--lib/X/Config/MissingParser.rakumod38
-rw-r--r--lib/X/Config/NotSupported.rakumod39
8 files changed, 653 insertions, 264 deletions
diff --git a/lib/Config.pm6 b/lib/Config.pm6
index 3db882e..65ba7b6 100644
--- a/lib/Config.pm6
+++ b/lib/Config.pm6
@@ -1,315 +1,539 @@
#! /usr/bin/env false
-use v6.c;
+use v6.d;
-use Config::Exception::FileNotFoundException;
-use Config::Exception::MissingParserException;
-use Config::Parser;
use Hash::Merge;
+use IO::Path::XDG;
+use Log;
-unit class Config is Associative;
+use Config::Parser;
+use X::Config::AbstractParser;
+use X::Config::FileNoExtension;
+use X::Config::FileNotFound;
+use X::Config::MissingParser;
+use X::Config::NotSupported;
-has Hash $!content = {};
-has Str $.path = "";
-has Str $.parser = "";
+#| A simple, flexible Config class.
+unit class Config does Associative;
-#| Clear the config.
-method clear()
-{
- $!content = {};
- $!path = "";
- $!parser = "";
-}
+#| The template for the configuration data structure.
+has %.template;
-method clone (
- --> Config
-) {
- Config.new(:$!path, :$!parser).read: $!content;
-}
+#| The actual values to represent the configuration items.
+has %.data;
-#| Return the entire config hash.
-multi method get()
-{
- return $!content;
-}
+#| The name for this Config object. The name is used for autodiscovery of
+#| configuration values from the shell environment and standard configuration
+#| file locations.
+has Str $.name;
-#| Fallback method in case the key is Nil. Will always return the default
-#| value.
-multi method get(Nil $key, Any $default = Nil)
-{
- $default;
-}
+#| Create a new Config object.
+method new (
+ #| The template for the Config object. In its simplest form, this is
+ #| just a Hash with all keys you want, including default values.
+ %template = {},
-#| Get a value from the config object. To get a nested
-#| key, use a . to descent a level.
-multi method get(Str $key, Any $default = Nil)
-{
- self.get($key.split(".").list, $default);
-}
+ #| The name of the name.
+ Str :$name,
-#| Get a value from the config object using a list
-#| to indicate the nested key to get.
-multi method get(List $keyparts, Any $default = Nil)
-{
- my $index = $!content;
+ #| Immediately set some Config values. This is rarely desired for most
+ #| use-cases.
+ :%data? is copy,
- for $keyparts.list -> $part {
- return $default unless defined($index{$part});
+ #| Try to read configuration data from the shell's environment.
+ Bool:D :$from-env = True,
- $index = $index{$part};
- }
+ #| Try to read configuration files from XDG_CONFIG_HOME and
+ #| XDG_CONFIG_DIRS.
+ Bool:D :$from-xdg = True,
- $index;
-}
+ --> Config:D
+) {
+ %data ||= %template;
-#| Get the name of the parser module to use for the
-#| given path.
-multi method get-parser(Str $path, Str $parser = "" --> Str)
-{
- return $parser if $parser ne "";
- return $!parser if $!parser ne "";
+ %data = merge-hash(%data, self!read-from-env(%template, :$name)) if $from-env && $name;
+ %data = merge-hash(%data, self!read-from-xdg-files(:$name)) if $from-xdg && $name;
- my $type = self.get-parser-type($path);
+ self.bless(
+ :%template,
+ :%data,
+ )
+}
- "Config::Parser::" ~ $type;
+#| Retrieve the entire Config object as a Hash.
+multi method get (
+ --> Hash:D
+) {
+ %!data
}
-multi method get-parser(IO::Path $path, Str $parser = "" --> Str)
-{
- samewith($path.absolute, $parser)
+#| Retrieve a specific Config item at $key.
+multi method get (
+ #| The key to check at.
+ Str:D $key,
+
+ #| A default value, in case the key does not exist.
+ Any $default = Nil,
+
+ --> Any
+) {
+ self.get($key.split('.'), $default)
}
-#| Get the type of parser required for the given path.
-multi method get-parser-type(Str $path --> Str)
-{
- given ($path) {
- when .ends-with(".yml") { return "yaml"; };
- }
+#| Retrieve a specific Config item specified by a list of nested keys.
+multi method get (
+ #| The list of nested keys.
+ @parts,
- my $file = $path;
+ #| A default value, in case the key does not exist.
+ Any $default = Nil,
- if (defined($path.index("/"))) {
- $file = $path.split("/")[*-1];
- }
+ --> Any
+) {
+ my $index = %!data;
- if (defined($file.index("."))) {
- return $file.split(".")[*-1].lc;
- }
+ for @parts {
+ return $default unless $index{$_}:exists;
- return "";
-}
+ $index = $index{$_};
+ }
-multi method get-parser-type(IO::Path $path --> Str)
-{
- samewith($path.absolute)
+ $index;
}
-#| Check wether a given key exists.
-multi method has(Str $key) {
- self.has($key.split(".").list);
+#| Check whether the Config object has a value at $key.
+multi method has (
+ #| The key to check the existence of.
+ Str:D $key,
+
+ --> Bool:D
+) {
+ self.has($key.split('.'));
}
-#| Check wether a given key exists using a list to supply
-#| the nested key to check.
-multi method has(List $keyparts)
-{
- my $index = $!content;
+#| Check whether the Config object has a value at the location specified in
+#| @parts.
+multi method has (
+ #| A list of the parts of the key to check the existence of.
+ @parts,
+
+ --> Bool:D
+) {
+ my $index = %!data;
- for $keyparts.list -> $part {
- return False unless defined($index{$part});
+ for @parts {
+ return False unless $index{$_}:exists;
+ last if $index ~~ Scalar;
- $index = $index{$part};
- }
+ $index = $index{$_};
+ }
- defined($index);
+ True;
}
-#| Return a sorted list of all available keys in the current Config.
-method keys()
-{
- my @keys;
+#| Get a flat list of all keys in the Config object.
+method keys (
+ --> Iterable:D
+) {
+ self!recurse-keys(%!data)
+}
- for $!content.keys -> $key {
- @keys.append: self.extract-keys($key);
- }
+#| Create a new Config object with %data merged in.
+multi method read (
+ #| A Hash of configuration data to merge in.
+ %data,
- @keys.sort;
+ --> Config:D
+) {
+ Config.new(
+ %!template,
+ :$!name,
+ data => merge-hash(%!data, %data),
+ :!read-from-env,
+ :!read-from-xdg,
+ )
}
-#| Reload the configuration. Requires the configuration to
-#| have been loaded from a file.
+#| Update the Config values from a given file.
multi method read (
- --> Config
+ #| The path to the configuration file.
+ IO() $path,
+
+ #| An explicit Config::Parser class to use. If left empty, it will be
+ #| guessed based on the file's extension.
+ Config::Parser:U $parser? is copy,
+
+ --> Config:D
) {
- die "Configuration was not loaded from a file, cannot reload" if $!path eq "";
+ X::Config::FileNotFound.new(:$path).throw if !$path.f;
+
+ # We need an implementation of Config::Parser, not the base
+ # Config::Parser role.
+ if ($parser.^name eq 'Config::Parser') {
+ $parser = self!parser-for-file($path);
+ }
- self.read($!path);
+ self.read(self!read-from-file($path, $parser));
}
-#| Load a configuration file from the given path. Optionally
-#| set a parser module name to use. If not set, Config will
-#| attempt to deduce the parser to use.
+#| Update the Config values from a list of files.
multi method read (
- Str $path,
- Str $parser = "",
- Bool :$skip-not-found = False,
- --> Config
+ #| A list of paths to configuration files to load.
+ @paths,
+
+ #| An explicit Config::Parser class to use. If left empty, it will be
+ #| guessed based on each file's extension.
+ Config::Parser:U $parser? is copy,
+
+ #| Silently skip over files which don't exist.
+ Bool:D :$skip-not-found = False,
+
+ --> Config:D
) {
- samewith($path.IO, $parser, :$skip-not-found)
+ my %data = %!data;
+
+ for @paths -> $path {
+ next if !$path.f && $skip-not-found;
+ X::Config::FileNotFound.new(:$path).throw if !$path.f;
+
+ # We need an implementation of Config::Parser, not the base
+ # Config::Parser role.
+ if ($parser.^name eq 'Config::Parser') {
+ $parser = self!parser-for-file($path);
+ }
+
+ %data = merge-hash(%data, self!read-from-file($path, $parser));
+ }
+
+ self.read(%data);
}
-#| Load a configuration file from the given path. Optionally
-#| set a parser module name to use. If not set, Config will
-#| attempt to deduce the parser to use.
-multi method read (
- IO::Path $path,
- Str $parser = "",
- Bool :$skip-not-found = False,
- --> Config
-) {
- if (!$path.f && !$skip-not-found) {
- Config::Exception::FileNotFoundException.new(path => $path.absolute).throw();
- }
+#| Set a $key to $value.
+multi method set (
+ #| The key to change the value of.
+ Str:D $key,
- $!parser = self.get-parser($path, $parser);
+ #| The new value of the key.
+ $value,
- try {
- CATCH {
- when X::CompUnit::UnsatisfiedDependency {
- Config::Exception::MissingParserException.new(
- parser => $!parser
- ).throw();
- }
- }
+ --> Config:D
+) {
+ self.set($key.split('.'), $value);
+}
- require ::($!parser);
+#| Set a specific Config item specified by a list of nested keys to $value.
+multi method set (
+ #| A list of parts of the key to change the value of.
+ @parts,
- self.read(::($!parser).read($path));
- }
+ #| THe new value of the key.
+ $value,
- self;
+ --> Config:D
+) {
+ my %data = %!data;
+ my $index := %data;
+
+ for @parts {
+ $index := $index{$_};
+ }
+
+ $index = $value;
+
+ Config.new(
+ %!template,
+ :$!name,
+ :%data,
+ :!read-from-env,
+ :!read-from-xdg,
+ )
}
-#| Read a list of paths. Will fail on the first file that fails to load for
-#| whatever reason.
-multi method read (
- List $paths,
- Str $parser = "",
- Bool :$skip-not-found = False,
- --> Config
+#| Remove a key from the Config object.
+multi method unset (
+ #| The key to remove.
+ Str:D $key,
+
+ --> Config:D
) {
- for $paths.list -> $path {
- samewith($path, $parser, :$skip-not-found);
- }
+ self.unset($key.split('.'));
+}
+
+#| Remove a key from the Config object.
+multi method unset (
+ #| A list of parts of the key to remove.
+ @parts,
- self;
+ --> Config:D
+) {
+ my %data = %!data;
+ my $index := %data;
+
+ for 0..(@parts.elems - 2) {
+ $index := $index{@parts[$_]};
+ }
+
+ $index{@parts[*-1]}:delete;
+
+ Config.new(
+ %!template,
+ :$!name,
+ :%data,
+ :!read-from-env,
+ :!read-from-xdg,
+ )
}
-#| Read a plain Hash into the configuration.
-multi method read (
- Hash $hash,
- --> Config
+#| Write the Config object to a file.
+method write (
+ #| The path to write the configuration values to.
+ IO::Path:D $path,
+
+ #| The Config::Parser object to use.
+ Config::Parser:U $parser,
+
+ --> Bool:D
) {
- $!content = merge-hash($!content, $hash);
+ if ($parser.^name eq 'Config::Parser') {
+ X::Config::AbstractParser.new.throw;
+ }
- return self;
+ $parser.write($path, %!data);
}
-#| Set a single key to a given value;
-multi method set(Str $key, Any $value)
-{
- self.set($key.split(".").list, $value);
-}
+#| Return the default Config::Parser implementation for the given file. This is
+#| based solely on the file's extension.
+method !parser-for-file (
+ #| The path to the file.
+ IO::Path:D $path,
-multi method set(List $keyparts, Any $value)
-{
- my $index := $!content;
+ --> Config::Parser:U
+) {
+ my $extension = $path.extension;
- for $keyparts.list -> $part {
- $index{$part} = {} unless defined($index{$part});
+ X::Config::FileNoExtension.new(:$path).throw unless $extension;
- $index := $index{$part};
- }
+ my $parser = "Config::Parser::$extension";
- $index = $value;
+ .info("Loading $parser") with $Log::instance;
- self;
-}
+ try require ::($parser);
-multi method unset(Str $key)
-{
- self.unset($key.split(".").Array);
-}
+ return ::($parser) unless ::($parser) ~~ Failure;
-multi method unset(@parts)
-{
- my %index := $!content;
- my $target = @parts.pop;
+ if (::($parser) ~~ Failure) {
+ given (::($parser).exception) {
+ when X::NoSuchSymbol {
+ X::Config::MissingParser.new(:$parser).throw;
+ }
+ default {
+ .alert("Failed to load $parser!") with $Log::instance;
+ }
+ }
+ }
- for @parts.list -> $part {
- %index{$part} = {} unless defined(%index{$part});
+ Config::Parser
+}
- %index := %index{$part};
- }
+#| Read configuration data from environment variables.
+method !read-from-env (
+ #| The template in use by the Config object. This is needed to generate
+ #| a list of the keys to check for.
+ %template,
- %index{$target}:delete if %index{$target}:exists;
+ #| The name of the application. This will be used to prefix the
+ #| environment variables used.
+ Str:D :$name is copy,
- self;
+ --> Hash:D
+) {
+ my %data;
+
+ $name ~= '.';
+
+ self!recurse-keys(%template)
+ .sort
+ .map(sub ($key) {
+ # Convert $key to something more reminiscient of
+ # Shell-style variable names.
+ my $var = "$name$key"
+ .subst('.', '_', :g)
+ .uc
+ ;
+
+ # Check if the env var exists.
+ .debug("Checking \$$var") with $Log::instance;
+
+ return unless %*ENV{$var}:exists;
+
+ # Insert the value from the env var into the data
+ # structure.
+ .info("Using \$$var for $key") with $Log::instance;
+
+ my @key-parts = $key.split('.');
+ my $index := %data;
+ my $cast := %template;
+
+ for @key-parts {
+ last if $index ~~ Scalar;
+
+ $index := $index{$_};
+ $cast := $cast{$_};
+ }
+
+ # Cast the value appropriately
+ given ($cast.WHAT) {
+ when Numeric { $index = +%*ENV{$var} }
+ when Str { $index = ~%*ENV{$var} }
+ when Bool { $index = ?%*ENV{$var} }
+ default { $index = %*ENV{$var} }
+ }
+ })
+ ;
+
+ %data;
}
-#| Write the current configuration to the given path. If
-#| no parser is given, it tries to use the parser that
-#| was used when loading the configuration.
-multi method write(IO::Path $path, Str $parser = "")
-{
- samewith($path.absolute, $parser)
+#| Read configuration data from a file.
+method !read-from-file (
+ #| The path to the file to read.
+ IO::Path $file,
+
+ #| An explicit Config::Parser to parse the file with. If left empty,
+ #| the Config::Parser implementation to use will be deduced from the
+ #| file's extension.
+ Config::Parser:U $parser,
+
+ --> Hash:D
+) {
+ if ($parser.^name eq 'Config::Parser') {
+ X::Config::AbstractParser.new.throw;
+ }
+
+ # Use the Parser to read the file contents.
+ $parser.read($file.absolute)
}
-multi method write(Str $path, Str $parser = "")
-{
- my $chosen-parser = self.get-parser($path, $parser);
+#| Check the XDG_CONFIG_DIRS and XDG_CONFIG_HOME locations for configuration
+#| files, and load any that are found.
+method !read-from-xdg-files (
+ #| The name of the application. This will be used for the filename to
+ #| look for.
+ Str:D :$name,
+
+ --> Hash:D
+) {
+ my %data;
- require ::($chosen-parser);
- return ::($chosen-parser).write($path, $!content);
+ # Generate a list of all potential config file locations, based on the
+ # XDG base directory spec.
+ my @files = xdg-config-dirs()
+ .reverse
+ .map(sub ($dir) {
+ (« $name "$name/config" » X~ < .json .toml .yaml >).map({
+ $dir.add($_)
+ }).Slip
+ })
+ ;
+
+ # Check each file.
+ for @files -> $file {
+ .debug("Checking $file") with $Log::instance;
+
+ # Nothing to do if the file doesn't exist.
+ next unless $file.f;
+
+ .info("Reading config from $file") with $Log::instance;
+
+ # Delegate to the file reader method.
+ %data = merge-hash(%data, self!read-from-file($file, self!parser-for-file($file)));
+ }
+
+ %data;
}
-multi method AT-KEY(::?CLASS:D: $key)
-{
- self.get($key);
+#| Get a flat list of all available keys.
+method !recurse-keys (
+ #| The internal data Hash to generate a list of keys from.
+ %data,
+
+ #| The prefix to use. Only relevant for the recursive nature of this
+ #| method.
+ $prefix = '',
+
+ --> Iterable:D
+) {
+ my @keys;
+
+ for %data.keys -> $key {
+ if (%data{$key} ~~ Hash) {
+ @keys.append(self!recurse-keys(%data{$key}, "$key."));
+ next;
+ }
+
+ @keys.append("$prefix$key")
+ }
+
+ @keys;
}
-multi method EXISTS-KEY(::?CLASS:D: $key)
+#| Implementation for Associative role. This is set here for the friendly error
+#| message that can be generated with it.
+method ASSIGN-KEY (::?CLASS:D: $key, $new)
{
- self.has($key);
+ X::Config::NotSupported(
+ call => 'ASSIGN-KEY',
+ help => 'To set a key, use Config.set($key, $value)',
+ ).new.throw
}
-multi method DELETE-KEY(::?CLASS:D: $key)
+#| Implementation for Associative role. This allows the user to retrieve a
+#| Configuration key in the same way they'd retrieve an element from a Hash.
+method AT-KEY (::?CLASS:D: $key)
{
- self.unset($key);
+ self.get($key)
}
-multi method ASSIGN-KEY(::?CLASS:D: $key, $new)
+#| Implementation for Associative role. This is set here for the friendly error
+#| message that can be generated with it.
+method DELETE-KEY (::?CLASS:D: $key)
{
- self.set($key, $new);
+ X::Config::NotSupported(
+ call => 'DELETE-KEY',
+ help => 'To remove a key, use Config.unset($key)',
+ ).new.throw
}
-multi method BIND-KEY(::?CLASS:D: $key, \new)
+#| Implementation for Associative role. This allows the user to check for the
+#| existence of a Configuration key in the same way they'd check existence of a
+#| Hash key.
+method EXISTS-KEY (::?CLASS:D: $key)
{
- self.set($key, new);
+ self.has($key)
}
-submethod extract-keys($key)
-{
- my $value = self.get($key);
- return $key if $value !~~ Iterable;
+=begin pod
- my @keys;
+=NAME Config
+=VERSION 3.0.0
+=AUTHOR Patrick Spek <p.spek@tyil.work>
- for $value.keys -> $nested-key {
- @keys.append: self.extract-keys("{$key}.{$nested-key}");
- }
+=begin LICENSE
+Copyright © 2020
- return @keys;
-}
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Lesser General Public License as published by the Free
+Software Foundation, version 3.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program. If not, see http://www.gnu.org/licenses/.
+=end LICENSE
+
+=end pod
-# vim: ft=perl6 sw=4 ts=4 et
+# vim: ft=perl6 sw=8 ts=8 noet
diff --git a/lib/Config/Parser.pm6 b/lib/Config/Parser.pm6
index d067513..5d0343b 100644
--- a/lib/Config/Parser.pm6
+++ b/lib/Config/Parser.pm6
@@ -1,26 +1,13 @@
#! /usr/bin/env false
-use v6.c;
+use v6.d;
-use Config::Exception::UnimplementedMethodException;
+unit class Config::Parser;
-class Config::Parser
-{
- #| Attempt to read the file at a given $path, and returns its
- #| parsed contents as a Hash.
- method read(Str $path --> Hash)
- {
- Config::Exception::UnimplementedMethodException.new(
- method => "read"
- ).throw();
- }
+#| Attempt to read the file at a given $path, and returns its
+#| parsed contents as a Hash.
+method read(IO() $path --> Hash) { … }
- #| Attempt to write the $config Hash at a given $path. Returns
- #| True on success, False on failure.
- method write(Str $path, Hash $config --> Bool)
- {
- Config::Exception::UnimplementedMethodException.new(
- method => "write"
- ).throw();
- }
-}
+#| Attempt to write the $config Hash at a given $path. Returns
+#| True on success, False on failure.
+method write(IO() $path, Hash $config --> Bool) { … }
diff --git a/lib/Config/Parser/NULL.pm6 b/lib/Config/Parser/NULL.pm6
index 2352cfc..2845e3c 100644
--- a/lib/Config/Parser/NULL.pm6
+++ b/lib/Config/Parser/NULL.pm6
@@ -1,41 +1,30 @@
#! /usr/bin/env false
-use v6.c;
+use v6.d;
use Config::Parser;
#| The Config::Parser::NULL is a parser to mock with for testing purposes.
#| It exposes an additional method, set-config, so you can set a config
#| Hash to return when calling `read`.
-class Config::Parser::NULL is Config::Parser
-{
- my %mock-config = ();
-
- #| Return the mock config, skipping the file entirely.
- multi method read(Str $path --> Hash)
- {
- %mock-config;
- }
+unit class Config::Parser::NULL is Config::Parser;
- multi method read(IO::Path $path --> Hash)
- {
- %mock-config;
- }
+my %mock-config;
- #| Set the mock config to return on read.
- method set-config(Hash $config)
- {
- %mock-config = $config;
- }
+#| Return the mock config, skipping the file entirely.
+multi method read(IO() $path --> Hash)
+{
+ %mock-config;
+}
- #| Return True, as if writing succeeded.
- multi method write(Str $path, Hash $config --> Bool)
- {
- True;
- }
+#| Set the mock config to return on read.
+method set-config(Hash $config)
+{
+ %mock-config = $config;
+}
- multi method write(IO::Path $path, Hash $config --> Bool)
- {
- True;
- }
+#| Return True, as if writing succeeded.
+multi method write(IO() $path, Hash $config --> Bool)
+{
+ True;
}
diff --git a/lib/X/Config/AbstractParser.rakumod b/lib/X/Config/AbstractParser.rakumod
new file mode 100644
index 0000000..712f5af
--- /dev/null
+++ b/lib/X/Config/AbstractParser.rakumod
@@ -0,0 +1,36 @@
+#! /usr/bin/env false
+
+use v6.d;
+
+unit class X::Config::AbstractParser is Exception;
+
+method message
+{
+ 'You must use an implementation class of Config::Parser, not the base role'
+}
+
+=begin pod
+
+=NAME X::Config::AbstractParser
+=VERSION 3.0.0
+=AUTHOR Patrick Spek <p.spek@tyil.work>
+
+=begin LICENSE
+Copyright © 2020
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Lesser General Public License as published by the Free
+Software Foundation, version 3.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program. If not, see http://www.gnu.org/licenses/.
+=end LICENSE
+
+=end pod
+
+# vim: ft=raku noet sw=8 ts=8
diff --git a/lib/X/Config/FileNoExtension.rakumod b/lib/X/Config/FileNoExtension.rakumod
new file mode 100644
index 0000000..7bf30f4
--- /dev/null
+++ b/lib/X/Config/FileNoExtension.rakumod
@@ -0,0 +1,38 @@
+#! /usr/bin/env false
+
+use v6.d;
+
+unit class X::Config::FileNoExtension is Exception;
+
+has IO::Path $.path;
+
+method message
+{
+ "The file at $!path does not have an extension, so no Config::Parser implementation can be deduced for it. Try setting an explicit parser."
+}
+
+=begin pod
+
+=NAME X::Config::FileNoExtension
+=VERSION 3.0.0
+=AUTHOR Patrick Spek <p.spek@tyil.work>
+
+=begin LICENSE
+Copyright © 2020
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Lesser General Public License as published by the Free
+Software Foundation, version 3.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program. If not, see http://www.gnu.org/licenses/.
+=end LICENSE
+
+=end pod
+
+# vim: ft=raku noet sw=8 ts=8
diff --git a/lib/X/Config/FileNotFound.rakumod b/lib/X/Config/FileNotFound.rakumod
new file mode 100644
index 0000000..68ae347
--- /dev/null
+++ b/lib/X/Config/FileNotFound.rakumod
@@ -0,0 +1,38 @@
+#! /usr/bin/env false
+
+use v6.d;
+
+unit class X::Config::FileNotFound is Exception;
+
+has IO::Path $.path;
+
+method message
+{
+ "Could not find file at $!path.absolute()"
+}
+
+=begin pod
+
+=NAME X::Config::FileNotFound
+=VERSION 3.0.0
+=AUTHOR Patrick Spek <p.spek@tyil.work>
+
+=begin LICENSE
+Copyright © 2020
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Lesser General Public License as published by the Free
+Software Foundation, version 3.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program. If not, see http://www.gnu.org/licenses/.
+=end LICENSE
+
+=end pod
+
+# vim: ft=raku noet sw=8 ts=8
diff --git a/lib/X/Config/MissingParser.rakumod b/lib/X/Config/MissingParser.rakumod
new file mode 100644
index 0000000..9d11cd5
--- /dev/null
+++ b/lib/X/Config/MissingParser.rakumod
@@ -0,0 +1,38 @@
+#! /usr/bin/env false
+
+use v6.d;
+
+unit class X::Config::MissingParser is Exception;
+
+has Str $.parser;
+
+method message
+{
+ "$!parser is not a valid parser. Are you sure its installed?"
+}
+
+=begin pod
+
+=NAME X::Config::MissingParser
+=VERSION 3.0.0
+=AUTHOR Patrick Spek <p.spek@tyil.work>
+
+=begin LICENSE
+Copyright © 2020
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Lesser General Public License as published by the Free
+Software Foundation, version 3.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program. If not, see http://www.gnu.org/licenses/.
+=end LICENSE
+
+=end pod
+
+# vim: ft=raku noet sw=8 ts=8
diff --git a/lib/X/Config/NotSupported.rakumod b/lib/X/Config/NotSupported.rakumod
new file mode 100644
index 0000000..f5c2d84
--- /dev/null
+++ b/lib/X/Config/NotSupported.rakumod
@@ -0,0 +1,39 @@
+#! /usr/bin/env false
+
+use v6.d;
+
+unit class X::Config::NotSupported is Exception;
+
+has Str $.call;
+has Str $.help;
+
+method message
+{
+ $!help
+}
+
+=begin pod
+
+=NAME X::Config::NotSupported
+=VERSION 3.0.0
+=AUTHOR Patrick Spek <p.spek@tyil.work>
+
+=begin LICENSE
+Copyright © 2020
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU Lesser General Public License as published by the Free
+Software Foundation, version 3.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
+PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
+details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program. If not, see http://www.gnu.org/licenses/.
+=end LICENSE
+
+=end pod
+
+# vim: ft=raku noet sw=8 ts=8