I have a class with a string attribute. The attribute represents a path to a file. I want to ensure this file exists before constructing an object. In the future, I may also want to throw additional checks at the file, too, like whether or not it's formatted properly.
At any rate, if the file doesn't exist, I want to throw a descriptive exception.
After some trial and error, I came up with this:
unit class Vim::Configurator;
sub file-check($file) {
die (X::IO::DoesNotExist.new(:path($file), :trying('new'))) if !$file.IO.f.Bool;
return True;
}
has Str:D $.file is required where file-check($_);
But there is more than one way to do this, as we all know.
Another option is to put the constraint logic into the new or build methods. This is OK, but this feels old school and I think I prefer having the logic for each attribute spelled out right alongside the attribute like in the first example.
A third option:
has Str:D $.file is required where *.IO.f.Bool == True;
This is nice and concise, but the error thrown is very inscrutable.
A fourth option is to use subset to constrain the attribute with something like this:
subset Vim::Configurator::File where *.IO.f.Bool == True;
unit class Vim::Configurator;
has Vim::Configurator::File $.file is required;
The error message thrown here isn't the greatest either. Plus it just feels weird to me.
I'm sure there are other ways to skin this cat and I'm wondering what others are doing and if there is anything superior to any of the methods mentioned above. Thanks.
A third option:
has Str:D $.file is required where *.IO.f.Bool == True;
This is nice and concise, but the error thrown is very inscrutable.
You can have a block in the where clause and throw there:
class A {
has Str:D $.file where { .IO.f orelse .throw }
}
A.new(file => "absent");
which gives something like
Failed to find 'absolute/path/to/absent' while trying to do '.f'
in block at ...
in block <unit> ...
In the where clause, $_ is the passed file string, i.e., "absent" above. Then its existence-as-a-file is checked; .IO.f fails if it cannot locate the file, so with orelse, the $_ will be the Failure it resulted in, which is then .thrown.
However, if the passed file string does exist but it's not a file but, e.g., a directory, then .IO.f won't fail but instead return False! Then the orelse will not switch to .throw because False is a defined value. In this case, we are back to an unhelpful message. To this end, we can first check existence, and deal with fileness separately:
class A {
has Str:D $.file where { .IO.e.not
?? fail qq|"$_" does not exist at all|
!! .IO.f or fail qq|"$_" is not a file| };
}
and then
>>> A.new(file => "absent")
"absent" does not exist at all
in block ...
>>> A.new(file => "existing_dir")
"existing_dir" is not a file
in block ...
You can override the default TWEAK or BUILD methods to test if the file exists, if not, die.
class File {
has Str $.path is required;
submethod BUILD(:$path) {
die 'File $path does not exist!' unless $path.IO.e; # Check if the file corresponding to the path exists
$!path := $path;
}
}
If you want more fine grained control, you can also write a factory that does the checking rather than doing it on the class level.
Further reading: https://docs.raku.org/language/objects#Object_construction
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With