diff options
-rw-r--r-- | CHANGELOG.md | 55 | ||||
-rw-r--r-- | META6.json | 23 | ||||
-rw-r--r-- | lib/Config.pm6 | 654 | ||||
-rw-r--r-- | lib/Config/Parser.pm6 | 29 | ||||
-rw-r--r-- | lib/Config/Parser/NULL.pm6 | 45 | ||||
-rw-r--r-- | lib/X/Config/AbstractParser.rakumod | 36 | ||||
-rw-r--r-- | lib/X/Config/FileNoExtension.rakumod | 38 | ||||
-rw-r--r-- | lib/X/Config/FileNotFound.rakumod | 38 | ||||
-rw-r--r-- | lib/X/Config/MissingParser.rakumod | 38 | ||||
-rw-r--r-- | lib/X/Config/NotSupported.rakumod | 39 | ||||
-rw-r--r-- | t/01-reading.t | 35 | ||||
-rw-r--r-- | t/02-getting.t | 18 | ||||
-rw-r--r-- | t/04-setting.t | 2 | ||||
-rw-r--r-- | t/05-null-parser.t | 10 | ||||
-rw-r--r-- | t/06-deduce-parser.t | 38 | ||||
-rw-r--r-- | t/07-keys.t | 2 | ||||
-rw-r--r-- | t/08-unsetting.t.t | 7 | ||||
-rw-r--r-- | t/10-template.t | 53 |
18 files changed, 794 insertions, 366 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 67252d4..db5fb95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,21 +5,58 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [UNRELEASED] +## [3.0.0] - 2020-07-02 -### Changed +`Config` has been rebuilt from the ground up. This is a relatively old +project of mine, which I used to get into Raku. I've learned many new things +in the past couple years, many things of which have been applied to this +project. + +Much of the user-facing interface is the same, however, breaking some backwards +compatibility was inevitable with some of the new ways I wanted `Config` to +function. Please read through this CHANGELOG carefully, and consider reading +the documentation if you encounter issues. If you have any questions or +remarks, you can also send an email to `~tyil/raku-devel@lists.sr.ht`. + +### Added + +- `.new` now (optionally) accepts a `Hash` which will be used as a template. + From this template, environment variables will be checked for existence, and + used if they exist. You must also set a `:name` attribute, which will be used + as a prefix for all the variables. This should make it easier to include + environment variable based configuration in applications. -- The `.read` and `.write` methods of a `Config` object now support `IO::Path` - as arguments for the `$path`, in addition to supporting the `Str` based - `$path` argument. *I intend to deprecate the `Str` variant in the future*. + Additionally, if the `:name` attribute is set, a number of standard + directories will be probed to see if there's a usable configuration file to + read. Which directories to probe is based on the [XDG Base Directory + Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). + + Both environment and XDG-path based auto-loading of configuration can be + turned off with `:!from-env` and `:!from-xdg` respectively. + +- [`Log`](https://modules.raku.org/dist/Log:cpan:TYIL) is now included to + provide some debug logging. Any application using this will get logging in + their preferred format through this. + +### Changed - Related links: +- The `Config` object is now immutable. Calling methods that alter the + configuration values (such as `.read`, `.set`, `.unset`) will now return a + new `Config` object. Raku has a very pretty [`.=` + operator](https://docs.raku.org/language/operators#infix_.=) that may come in + handy! - - [`GitHub#5`](https://github.com/scriptkitties/p6-Config/issues/5) +- The `Exception` classes are now in `X::Config`, and the redundant `Exception` + suffix has been removed. - Special thanks to: +- When setting an explicit parser, the parser must be given as a type object, + instead of a `Str`. This will lead to potential issues being known at + compile-time, rather than runtime. - - [taboege](https://github.com/taboege) +- `Config`'s license has changed from `AGPL-3.0` to `LGPL-3.0-only`. The Lesser + General Public License allows use of `Config`, even in closed code bases. + This should make the module more usable for all sorts of people, while still + maintaining a strong focus on keeping it Free Software. ## [2.1.0] - 2018-08-26 @@ -1,26 +1,29 @@ { - "api": "2", + "api": "3", "auth": "github:scriptkitties", "authors": [ "Patrick Spek <p.spek@tyil.work>" ], "depends": [ - "Hash::Merge:api<1>:version<1.0.0+>" + "Hash::Merge:version<1.0.1>", + "IO::Path::XDG:version<0.2.0>", + "Log:auth<cpan:TYIL>:version<0.3.0>" ], "description": "Extensible library for reading and writing configuration files.", - "license": "GPL-3.0", + "license": "LGPL-3.0-only", "name": "Config", - "perl": "6.c", + "perl": "6.d", "provides": { "Config": "lib/Config.pm6", - "Config::Exception::FileNotFoundException": "lib/Config/Exception/FileNotFoundException.pm6", - "Config::Exception::MissingParserException": "lib/Config/Exception/MissingParserException.pm6", - "Config::Exception::UnimplementedMethodException": "lib/Config/Exception/UnimplementedMethodException.pm6", "Config::Parser": "lib/Config/Parser.pm6", - "Config::Parser::NULL": "lib/Config/Parser/NULL.pm6" + "Config::Parser::NULL": "lib/Config/Parser/NULL.pm6", + "X::Config::AbstractParser": "lib/X/Config/AbstractParser.rakumod", + "X::Config::FileNoExtension": "lib/X/Config/FileNoExtension.rakumod", + "X::Config::FileNotFound": "lib/X/Config/FileNotFound.rakumod", + "X::Config::MissingParser": "lib/X/Config/MissingParser.rakumod", + "X::Config::NotSupported": "lib/X/Config/NotSupported.rakumod" }, "resources": [ - ], "source-url": "https://github.com/scriptkitties/p6-Config.git", "tags": [ @@ -30,5 +33,5 @@ "test-depends": [ "Test::META" ], - "version": "2.1.0" + "version": "3.0.0" }
\ No newline at end of file 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 diff --git a/t/01-reading.t b/t/01-reading.t index 722d6eb..ba9bd55 100644 --- a/t/01-reading.t +++ b/t/01-reading.t @@ -1,28 +1,17 @@ -#! /usr/bin/env perl6 +#! /usr/bin/env raku -use v6.c; +use v6.d; use Test; -plan 6; +plan 4; use Config; +use Config::Parser::NULL; -my Config $config = Config.new(); -my Str $null-parser = "Config::Parser::NULL"; +my Config $config = Config.new; +my Config::Parser $null-parser = Config::Parser::NULL; -throws-like { $config.read("t/files/none") }, Config::Exception::FileNotFoundException, "Reading nonexisting file"; -throws-like { $config.read("t/files/config", "Config::Parser:NoSuchParserForTest") }, Config::Exception::MissingParserException, "Using non-existing parser"; - -subtest ".read allows for non-fatal execution with skip-not-found set", { - plan 3; - - my %old = $config.get; - my $result = $config.read("t/files/none", $null-parser, :skip-not-found); - - ok $result, "Result is ok"; - is-deeply $result.get, %old, "Config did not change"; - isa-ok $result, Config, ".read returned a Config"; -} +throws-like { $config.read('t/files/none'.IO) }, X::Config::FileNotFound, 'Reading nonexisting file'; my %hash = %( "a" => "a", @@ -33,9 +22,9 @@ my %hash = %( $config.read: %hash; -is-deeply $config.get, %hash, "Correctly sets hash"; +is-deeply $config.get, %hash, 'Correctly sets hash'; -$config.read: %( +$config.=read: %( "b" => %( "d" => "another", ), @@ -52,7 +41,7 @@ is-deeply $config.get, %( subtest { plan 3; - ok $config.read(("t/files/config", "t/files/config.yaml"), $null-parser, :skip-not-found), "All paths exist"; - ok $config.read(("t/files/config", "t/files/none", "t/files/config.yaml"), $null-parser, :skip-not-found), "At least one path exists"; - ok $config.read(("t/files/none", "t/files/none.yaml"), $null-parser, :skip-not-found), "No paths exist"; + ok $config.read(("t/files/config".IO, "t/files/config.yaml".IO), $null-parser, :skip-not-found), "All paths exist"; + ok $config.read(("t/files/config".IO, "t/files/none".IO, "t/files/config.yaml".IO), $null-parser, :skip-not-found), "At least one path exists"; + ok $config.read(("t/files/none".IO, "t/files/none.yaml".IO), $null-parser, :skip-not-found), "No paths exist"; }, "Read with a List of paths"; diff --git a/t/02-getting.t b/t/02-getting.t index 487b150..04859e2 100644 --- a/t/02-getting.t +++ b/t/02-getting.t @@ -18,17 +18,17 @@ $config.read({ is $config.get("a"), "a", "Get simple key"; is $config.get("b.c"), "c", "Get nested key"; -is $config.get("nonexistant", "test"), "test", "Get nonexistant key with default"; -ok $config.get("nonexistant") === Nil, "Get nonexistant key"; +is $config.get("nonexistent", "test"), "test", "Get nonexistent key with default"; +ok $config.get("nonexistent") === Nil, "Get nonexistent key"; is $config.get(["a"]), "a", "Get simple key by array"; is $config.get(["b", "c"]), "c", "Get nested key by array"; -is $config.get(["nonexistant"], "test"), "test", "Get nonexistant key by array with default"; -ok $config.get(["nonexistant"]) === Nil, "Get nonexistant key by array"; +is $config.get(["nonexistent"], "test"), "test", "Get nonexistent key by array with default"; +ok $config.get(["nonexistent"]) === Nil, "Get nonexistent key by array"; -is $config.<a>, "a", "Get simple key via associative index"; -is $config.<b.c>, "c", "Get nested key via associative index"; -ok $config.<nonexistant> === Nil, "Get nonexistant key via associative index"; +is $config<a>, "a", "Get simple key via associative index"; +is $config<b.c>, "c", "Get nested key via associative index"; +is $config<nonexistent>, Nil, "Get nonexistent key via associative index"; -is $config.get(Nil, "test"), "test", "Attempt .get with Nil key with default"; -ok $config.get(Nil) === Nil, "Attempt to .get with Nil key"; +is $config.get('nonexistent', "test"), "test", "Attempt .get with nonexistent key with default"; +is $config.get('nonexistent'), Nil, "Attempt to .get with nonexistent key"; diff --git a/t/04-setting.t b/t/04-setting.t index 77039ca..96ab7c3 100644 --- a/t/04-setting.t +++ b/t/04-setting.t @@ -8,7 +8,7 @@ plan 4; use Config; -my $config = Config.new(); +my $config = Config.new; is $config.set("a", "test").get("a"), "test", "Setting simple key"; is $config.set("b.c", "test").get("b.c"), "test", "Setting nested key"; diff --git a/t/05-null-parser.t b/t/05-null-parser.t index 93be062..72aedf8 100644 --- a/t/05-null-parser.t +++ b/t/05-null-parser.t @@ -8,22 +8,22 @@ use Config::Parser::NULL; plan 3; -::("Config::Parser::NULL").set-config({ +Config::Parser::NULL.set-config({ "a" => "a", "b" => { "c" => "c" } }); -my Config $config = Config.new(); +my Config $config = Config.new; -ok $config.read("t/files/config", "Config::Parser::NULL"), "Attempt to read a file with Config::Parser::NULL"; +ok $config.read('t/files/config'.IO, Config::Parser::NULL), "Attempt to read a file with Config::Parser::NULL"; -is-deeply $config.get(), { +is-deeply $config.get, { "a" => "a", "b" => { "c" => "c" } }, "Check read config from Config::Parser::NULL"; -ok $config.write("t/t/t"), "Attempt to write a file with Config::Parser::NULL"; +ok $config.write('t/t/t'.IO, Config::Parser::NULL), "Attempt to write a file with Config::Parser::NULL"; diff --git a/t/06-deduce-parser.t b/t/06-deduce-parser.t deleted file mode 100644 index d1c065a..0000000 --- a/t/06-deduce-parser.t +++ /dev/null @@ -1,38 +0,0 @@ -#! /usr/bin/env perl6 - -use v6.c; -use Test; - -use Config; - -plan 4; - -my Config $config = Config.new(); - -subtest "Unknown parser type" => { - plan 1; - - is $config.get-parser-type("config"), "", "Type for plain file without extension"; -}; - -subtest "Check parser type by file extension" => { - plan 2; - - is $config.get-parser-type("config.yaml"), "yaml", "Should return extension"; - is $config.get-parser-type("config.TOML"), "toml", "Should return lower-cased extension"; -}; - -subtest "Check parser type for edge-cases defined in get-parser-type" => { - plan 1; - - is $config.get-parser-type("config.yml"), "yaml", "yml --> yaml"; -}; - -subtest "Returns correct fully qualified module name" => { - plan 4; - - is $config.get-parser("config"), "Config::Parser::", "Empty parser on unknown type"; - is $config.get-parser("config.yaml"), "Config::Parser::yaml", "Extension when available"; - is $config.get-parser("config.TOML"), "Config::Parser::toml", "Lowercased extension"; - is $config.get-parser("config", "Config::Parser::NULL"), "Config::Parser::NULL", "Given string"; -}; diff --git a/t/07-keys.t b/t/07-keys.t index 99bae81..ddc4fdf 100644 --- a/t/07-keys.t +++ b/t/07-keys.t @@ -19,6 +19,6 @@ my Config $c .= new.read: %( my @keys = < a b c.a c.b >; -is $c.keys, @keys, ".keys returns a list of all keys"; +is $c.keys.sort, @keys, ".keys returns a list of all keys"; # vim: ft=perl6 noet diff --git a/t/08-unsetting.t.t b/t/08-unsetting.t.t index 330ba5a..da76164 100644 --- a/t/08-unsetting.t.t +++ b/t/08-unsetting.t.t @@ -3,7 +3,7 @@ use v6.c; use Test; -plan 8; +plan 4; use Config; @@ -16,11 +16,6 @@ $config.read: %( ), ); -ok $config<a>:exists, "'a' exists"; -ok $config<a>:delete, "'a' gets deleted"; -nok $config<a>:exists, "'a' no longer exists"; -ok $config<c>:exists, "'c' remains untouched"; - ok $config.has("c.d"), "'c.d' exists"; ok $config.unset("c.d"), "'c.d' gets deleted"; nok $config.has("c.d"), "'c.d' no longer exists"; diff --git a/t/10-template.t b/t/10-template.t new file mode 100644 index 0000000..3baa15b --- /dev/null +++ b/t/10-template.t @@ -0,0 +1,53 @@ +#!/usr/bin/env raku + +use v6.d; + +use Test; + +use Config; + +plan 2; + +subtest 'Flat template', { + plan 6; + + my $config = Config.new({ + foo => Any, + bar => Any, + }, :name<raku-config>); + + ok $config, 'Config object instantiated'; + + ok $config.has('foo'), 'Config contains "foo"'; + ok $config.has('bar'), 'Config contains "bar"'; + + nok $config.has('alpha'), 'Config does not contain "alpha"'; + nok $config.has('beta'), 'Config does not contain "beta"'; + + is $config.keys.sort, < bar foo >, 'Config.keys is correct'; +} + +subtest 'Nested template', { + plan 7; + + my $config = Config.new({ + foo => { + alpha => Any, + }, + bar => { + beta => Any, + }, + baz => Any, + }, :name<raku-config>); + + ok $config, 'Config object instantiated'; + + ok $config.has('foo'), 'Config contains "foo"'; + ok $config.has('foo.alpha'), 'Config contains "foo.alpha"'; + ok $config.has('baz'), 'Config contains "baz"'; + + nok $config.has('omega.phi'), 'Config does not contain "omega.phi"'; + nok $config.has('omega'), 'Config does not contain "omega"'; + + is $config.keys.sort, < bar.beta baz foo.alpha >, 'Config.keys is correct'; +} |