### ----------------------------------------------------
### -- SQL::Abstract
### -- Licenses: Artistic-2.0
### -- Authors: Leon Timmermans
### -- File: lib/SQL/Abstract.rakumod
### ----------------------------------------------------
### -- Chunk 4 of 4
Any) $assigns is copy, Conflict::Targets(Cool) $targets) {
		my ($extra-identifiers, Expression @extra-values) := extract-where($!where);
		if (@extra-values) {
			my @pairs = $extra-identifiers.elements Z=> @extra-values;
			my $extra-assigns = Assigns.COERCE(@pairs);
			$assigns = $extra-assigns.merge($assigns);
		}

		my $query = upsert($!source, $assigns, $targets, :$!common);
		Builder::Insert.new(:$!renderer, :$query);
	}

	method delete() {
		my ($target, $using) = split-source($!source);
		my $query = Delete.new(:$target, :$using, :$!where);
		Builder::Delete.new(:$!renderer, :$query);
	}
}

class Builder {
	has Renderer:D $.renderer is required;
	has Common $.common;

	multi method new(Renderer::SQL:U :renderer($renderer-class)!, *%arguments) {
		my $renderer = $renderer-class.new(|%arguments);
		self.bless(:$renderer);
	}
	multi method new(Str:D :renderer($renderer-name)!, *%arguments) {
		my $renderer-class = renderer-for($renderer-name);
		my $renderer = $renderer-class.new(|%arguments);
		self.bless(:$renderer);
	}

	method with(Common(Any) $with) {
		my $common = $!common.merge($with);
		self.new(:$!renderer, :$common);
	}

	method on(Source(Any) $source) {
		Builder::Source.new(:$!renderer, :$!common, :$source);
	}
}

=begin pod

=head1 Name

SQL::Abstract - Generate SQL from Raku data structures

=head1 Synopsis

=begin code :lang<raku>

use SQL::Abstract;

my $abstract = SQL::Abstract.new(:renderer<sqlite>);
my $query = $abstract.select('table', <foo bar>, { :id(3) });
my $result = $dbh.query($result.sql, $result.arguments);

my $join = { :left<books>, :right<authors>, :using<author_id> };
my $result = $abstract.select($join, ['books.name', 'authors.name'], { :cost('<' => 10) });

=end code

=head1 Description

SQL::Abstract abstracts the generation of SQL queries. Fundamentally its functionality has three components

=item1 An AST to represent SQL queries
=item1 A set of helpers to convert standard raku datatypes into such an AST
=item1 A renderer to turn that AST into an SQL query

It should be able to represent any C<SELECT>, C<UPDATE>, C<INSERT>, or C<DELETE> query that is valid in both Postgresql and SQLite. This subset should be generally portable to other databases as well.

=head1 Class SQL::Abstract

This is the main class of the module.

=head3 new(:$renderer!, Bool :$quoting, *%renderer-args)

This creates a new C<SQL::Abstract> object.

It supports at least two named arguments:

=begin item1
C<Bool :$quote>

=end item1

=begin item1
C<:$renderer>

This can either be a C<Renderer> type-object, or the name of such an object. Valid values include:

=begin item2
C<'postgres'>/C<SQL::Abstract::Renderer::Postgres>

=end item2

=begin item2
C<'sqlite'>/C<SQL::Abstract::Renderer::SQLite>

=end item2

=begin item2
C<'mysql'>/C<SQL::Abstract::Renderer::MySQL>

=end item2


The postgres renderer takes an additional argument C<:$placeholders>, which takes one of the following values:

=begin item2
C<'dbi'>/C<SQL::Abstract::Placeholders::DBI>

This will use DBI style C<(?, ?)> placeholders
=end item2

=begin item2
C<'postgres'>/C<SQL::Abstract::Placeholders::Postgres>

This will use Postgres style C<($1, $2)> placeholders.
=end item2

It also takes an optional argument C<:$quoting>, if enabled table and column names will always be quoted.

=end item1

=head2 select

=begin code :lang<raku>

method select(Source(Any) $from, Column::List(Any) $columns = *, Conditions(Any) $where?, Common(Any) :$common,
Distinction(Any) :$distinct, GroupBy(Any) :$group-by, Conditions(Any) :$having, Window::Clauses(Any) :$windows,
Compound(Pair) :$compound, OrderBy(Any) :$order-by, Limit :$limit, Offset :$offset, Locking(Any) :$locking)

=end code

This will generate a C<SELECT> query. It will select C<$columns> from C<$from>, filtering by $conditions.

=begin code :lang<raku>

my $join = { :left<books>, :right<authors>, :using<author_id> };
my $result = $abstract.select($join, ['books.name', 'authors.name'], { :cost{ '<' => 10 } });
# SELECT books.name, authors.name FROM books INNER JOIN authors USING (author_id) WHERE cost < 10

my @columns = [ 'name', :sum{ :function<count>, :arguments(*) } ];
my $counts = $$abstract.select('artists', @columns, { :name(like => 'A%') }, :group-by<name>, :order-by(:sum<desc>));
# SELECT name, COUNT(*) as sum FROM artists WHERE name LIKE 'A%' GROUP BY name ORDER BY sum DESC

=end code

=head2 update

=begin code :lang<raku>

method update(Table(Any) $target, Assigns(Any) $assigns, Conditions(Any) $where?,
Common(Any) :$common, Source(Any) :$from, Column::List(Any) :$returning)

=end code

This will update C<$target> by assigning the columns and values from C<$set> if they match C<$where>, returning C<$returning>.

=begin code :lang<raku>

$abtract.update('artists', { :name('The Artist (Formerly Known as Prince)') }, { :name<Prince> });
# UPDATE artists SET name = 'The Artist (Formerly Known as Prince)' WHERE name = 'Prince'

=end code

=head2 insert

=head3 Map insertion

=begin code :lang<raku>

method insert(Table(Any) $target, Assigns(Any) $values, Common(Any) :$common,
Overriding(Str) :$overriding, Conflicts(Any) :$conflicts, Column::List(Any) :$returning)

=end code

Inserts the values in C<$values> into the table C<$target>, returning the columns in C<$returning>

=begin code :lang<raku>

$abstract.insert('artists', { :name<Metallica> }, :returning(*));
# INSERT INTO artists (name) VALUES ('Metallica') RETURNING *

=end code

=head3 List insertions

=begin code :lang<raku>

method insert(Table(Any) $target, Identifiers(Any) $columns, Rows(List) $rows, Common(Any) :$common,
Overriding(Str) :$overriding, Conflicts(Any) :$conflicts, Column::List(Any) :$returning)

=end code

Insert into C<$target>, assigning each of the values in Rows to a new row in the table. This way one can insert a multitude of rows into a table.

=begin code :lang<raku>

$abstract.insert('artists', ['name'], [ ['Metallica'], ['Motörhead'] ], :returning(*));
# INSERT INTO artists (name) VALUES ('Metallica'), ('Motörhead') RETURNING *

$abstract.insert('artists', List, [ [ 'Metallica'], ], :returning<id>);
# INSERT INTO artists VALUES ('Metallica') RETURNING id

=end code

=head3 Select insertion

=begin code :lang<raku>

method insert(Table(Any) $target, Identifiers(Any) $columns, Select(Map) $select, Common(Any) :$common,
Overriding(Str) :$overriding, Conflicts(Any) :$conflicts, Column::List(Any) :$returning)

=end code

This selects from a (usually different) table, and inserts the values into the table.

=begin code :lang<raku>

$abstract.insert('artists', 'name', { :from<new_artists>, :columns<name> }, :returning(*));
# INSERT INTO artists (name) SELECT name FROM new_artists RETURNING *

=end code

=head2 upsert

=begin code :lang<raku>

method upsert(Table(Any) $target, Assigns(Any) $values, Conflict::Targets $targets, Common(Any) :$common,
Overriding(Str) :$overriding, Column::List(Any) :$returning)

=end code

This upserts data into the table: it tries to insert data but if it fails because of a unique index constraint on one of C<$targets> it will update the other columns instead.

=begin code :lang<raku>

$abstract.upsert('phone_numbers', { :$name, :$number}, ['name']);
# INSERT INTO phone_numbers (name, number) VALUES (?, ?) ON CONFLICT (name) UPDATE number = ?

=end

=head2 delete

=begin code :lang<raku>

method delete(Table(Any) $target, Conditions(Any) $where, Common(Any) :$common,
Source(Any) :$using, Column::List(Any) :$returning)

=end code

This deletes rows from the database, optionally returning their values.

=begin code :lang<raku>

$abstract.delete('artists', { :name<Madonna> });
# DELETE FROM artists WHERE name = 'Madonna'

=end code

=head1 Builder Types

Builder objects generate SQL statements using the builder pattern

=begin code :lang<raku>

my $builder = SQL::Abstract::Builder(:renderer<sqlite>);
my $query = $builder.on('table').where({ :1a, :b<bar> }).update({ :$b }).build;
# "UPDATE table SET b = ? WHERE a = 1 AND b = 'bar'", [ $b ]

=end code

=head2 Builder

=head3 on

=code method on(Source(Any) $name --> Builder::Source)

This will return a source builder for the indicated table or join

=head3 with

=code method with(Common(Any) $cte --> Builder)

This returns a new C<Builder> object with common table expressions injected into it.

=head2 Builder::Source

=head3 select

=begin code :lang<raku>

multi method select(Column::List(Any) $columns --> Builder::Select)
multi method select(*@columns --> Builder::Select)

=end code

This returns a select builder with the given columns.

=head3 insert

=begin code :lang<raku>

multi method insert(Assigns(Any) $values)
multi method insert(Identifiers(Any) $main-columns, Rows(List) $main-rows)
multi method insert(Identifiers(Any) $columns, Value::Default $)

=end code

This returns a select builder with the given values.

=begin code :lang<raku>

$builder.insert({ :1a, :b<foo>, :c(Nil) }).build

=end code

=head3 update

=code method update(Assigns(Any) $assigns --> Builder::Update)

This returns a select builder with the given assignments.

=begin code :lang<raku>

$builder.on('table').where({ :1a }).update({ :2a })
$builder.on('table').update({ :2a }).where({ :1a })

=end code

=head3 delete

=code method delete(--> Builder::Delete)

This return a new delete builder for the given table

=head3 upsert

=code method upsert(Assigns $assigns, Conflict::Targets $targets --> Builder::Insert)

This returns a new insert builder with an upsert clause.

=head3 join

=begin code :lang<raku>

method join(Source(Any) $source, Join::Conditions() :$on)
method join(Source(Any) $source, Identifiers() :$using)
method join(Source(Any) $source, Bool :$natural)
method join(Source(Any) $source, Bool :$cross)

=end code

Join the target to another table.

=begin code :lang<raku>

$on.join('Bar', :using<id>);
$on.join('Bar', :on{ 'Foo.id' => 'Bar.foo_id' });

=end code

=head3 where

=code method where(Conditions(Any) $where --> Builder::On)

This adds a condition to the subsequent operation, or if it's an insert operation adds those values if possible.

=head2 Builder::Select

=item method distinct(Distinction(Any) $distinct --> Builder::Select)
=item method where(Conditions(Any) $where --> Builder::Select)
=item method group-by(GroupBy(Any) $group-by --> Builder::Select)
=item method having(Conditions(Any) $having --> Builder::Select)
=item method windows(Window::Clauses(Any) $windows --> Builder::Select)
=item method compound(Compound(Pair) $compound --> Builder::Select)
=item method order-by(OrderBy(Any) $order-by --> Builder::Select)
=item method limit(Limit(Any) $limit --> Builder::Select)
=item method offset(Offset(Any) $offset --> Builder::Select)
=item method locking(Locking(Any) $locking --> Builder::Select)
=item method insert(Table(Any) $target, Identifiers(Any) $columns? --> Builder::Insert)

=head2 Builder::Insert

=item method overriding(Overriding(Str) $overriding --> Builder::Insert)
=item method conflicts(Conflicts(Any) $conflicts --> Builder::Insert)
=item method returning(Column::List(Any) $returning --> Builder::Insert)

=head2 Builder::Update

=item method from(Update::From(Any) $from --> Builder::Update)
=item method where(Conditions(Any) $where --> Builder::Update)
=item method returning(Column::List(Any) $returning --> Builder::Update)

=head2 Builder::Delete

=item method using(Update::From(Any) $using --> Builder::Delete)
=item method where(Column::List(Any) $where --> Builder::Delete)
=item method returning(Column::List(Any) $returning --> Builder::Delete)

=head1 Helper types

SQL::Abstract uses various helper types that will generally coerce from basic datastructures:

=head2 SQL::Abstract::Identifier

This represents an identifier (typically a table name or column name, or an alias for such). It can be coerced from a string (e.g. C<"foo"> or C<"foo.bar">).

=head2 SQL::Abstract::Identifiers

This takes either a list of C<Identifier()>, or a single C<Identifier()>.

=begin code :lang<raku>

my SQL::Abstract::Identifiers() $identifiers = <name email website>;

=end code

=head2 SQL::Abstract::Source

A source is source of data, usually a table or a join. If not passed as a C<Source> object it will upconvert from the following types:

=begin item1
Str

This represents the named table, e.g. C<"my_table">.
=end item1

=begin item1
List

This represents the named table, with the elements of the list representing the components of the table name. E.g. C<< <bar baz> >> is equivalent to C< "bar.baz" >.
=end item1

=begin item1
Pair (Str => Identifier(Cool))

This will rename the table in the value to the name in the key.
=end item1

=begin item1
Pair (Str => Select(Map))

This will use the result of a subquery as if it's a table.
=end item1

=begin item1
Pair (Str => Function(Map))

This will use the function as a subquery.
=end item1

=begin item1
Map

This will join two `Source`s, named C<left> and C<right>, it requires one of the following entries to join them on:

=item3 Join::Conditions() :$on
=item3 Identifiers() :$using
=item3 Bool :$natural
=item3 Bool :$cross

e.g. C<< { :left<artist>, :right<album>, :using<album_id> } >> or C<< { :left<artist>, :right<album>, :on{'artist.id' => 'album.artist_id'} } >>

The first three joiners take an optional C<:$type> argument that can be any of C<"inner">/C<Join::Type::Inner>, C<"left">/C<Join::Type::Left>, C<"right">/C<Join::Type::Right> or C<"full">/C<Join::Type::Full>.

=end item1

=head2 SQL::Abstract::Table does SQL::Abstract::Source

This role takes the same conversions as C<Source>, but only the ones that represent a table. Unlike other sources, this can be used for mutating operations (update/insert/delete).

=head2 SQL::Abstract::Column::List

=begin code :lang<raku>

my Column::List() $columns = ('name', :number(:count(*)));
# name, COUNT(*) AS number;

=end code

This is a list of items representing a column. Each item can either be a:
much like C<Identifiers>, however it will accept not just identifiers but any expression (e.g. comparisons, function calls, etc…). If given a pair it will rename the value to the key (C<value AS key>). A whatever-star will represent all columns.

=head2 SQL::Abstract::Conditions

=begin code :lang<raku>

my Conditions() $conditions = { :name(:like<%leon%>), :age(25..45), :country('nl'|'be'|lu') };
# name LIKE '%leon%' AND AGE BETWEEN 25 AND 45 AND country IN('nl', 'be', 'lu')

=end code

This is a pair, a list of pairs, a hash or an C<Expression>. In the former three cases, the key (called left in the rest of this section) shall be an C<Identifier()> designating a column name, or an C<Expression>. The right hand side can be one of several types:

=head3 Expression

This will be used as-is

=head3 Any:U

This will check if the left expression is C<NULL>; C<:left(Any)> equals C<left IS NULL>.

=head3 Pair

This will use the key as operator to compare left against another value or expression. E.g. C<< :left('<' => 42) >> renders like C<< left < 42 >>. The following keys are known:

=item C<=>
=item C<!=>
=item C<< <> >>
=item C<< < >>
=item C<< <= >>
=item C<< > >>
=item C<< >= >>
=item C<like>
=item C<not-like>
=item C<distinct>
=item C<not-distinct>
=item C<||>
=item C<< -> >>
=item C<<< ->> >>>
=item C<< #> >>
=item C<<< #>> >>>
=item C<*>
=item C</>
=item C<%>
=item C<+>
=item C<->
=item C<&>
=item C<|>
=item C<<< << >>>
=item C<<< >> >>>

A few operators are not binary operators as such.

=begin item1
C<null>

will do C<IS NULL>, or C<IS NOT NULL> if its argument is false(C<:!null>).
=end item1

=begin item1
C<in>/C<not-in>

This takes a list of values that the column will be compared to. E.g. C<:in(1, 2, 3)>.
=end item1

=begin item1
C<and>/C<or>

The logical operators take a list or arguments, that are all expanded like pair values. So C<< :foo(:and('>' => 3, '<' => 42)) >> renders as C<< foo > 3 AND foo < 42 >>
=end item1

=head3 Range

This will check if a value is in a certain range. E.g. C<:left(1..42)> will render like C<left BETWEEN 1 AND 42>.

=head3 Junction

This will check against the values in the function. E.g. C<:left(1|2|4)> will render like C<left IN (1, 2, 4)>.

=head3 Capture

This will be expanded as a capture expression (. E.g. C<:left(\'NOW()')> will render like C<left = NOW()>. If it's an C<:op> expression, the column will be inserted as first/left operand: C<< :left(\(:op('<' => 42))) >> renders like C<< left < 42 >>.

=head3 Any

If none of the above options match, the value will be compared to as is (as a placeholder). C<:left(2)> will render equivalent to C<left = 2>.

=head2 SQL::Abstract::Assigns

This takes a list of pairs, or a hash. The keys shall be a value or an expression. E.g. C<< :name<author>, :id(SQL::Abstract::Values::Default), :timestamp(\'NOW()') >>

=head2 SQL::Abstract::OrderBy

This takes a list of things to sort by. Much like C<Column::List> this accepts identifiers and expressions, but C<*> isn't allowed and pair values are interpreted as order modifier (e.g. C<:column<desc>>). A hash element will be expanded as well (e.g. C<< { :column<column_name>, :order<desc>, :nulls<last> } >>)

=head2 SQL::Abstract::Common

This represents a common table expression. It converts from a pair or a list of pairs, with the keys being the name and the values being either a table name, a select hash or an C<SQL::Abstract::Query> object.

=begin code :lang<raku>

my Common() $cte = recent => { :from<users>, :columns('name', :count(:count(*)), :group-by(name) };
# WITH recent AS (SELECT name, COUNT(*) AS count FROM users GROUP BY name);

=end code

=head2 SQL::Abstract::Locking

This takes one or more locking clauses. A locking clause is usually taken ... strings: C<'update'>, C'<no key update'>, C<'share'>, C<'key share'>, but it can also take a pair of stregth

=head2 SQL::Abstract::GroupBy

This takes a list of grouping elements. Usually these are just columns, but they may also be arbitrary expressions (inclusing lists of columns). A pair is taken as a function call with the key as function name and the value as arguments.

=head2 SQL::Abstract::Conflicts

This represents one or more upsert clause. It can either be the string C<'nothing'>, or a pair with the columns as keys and an C<Assigns(Map)>.

=begin code :lang<raku>

my SQL::Abstract::Conflicts = <name organization> => { :$email };
# ON CONFLICT (name, organization) DO UPDATE email = ?

=end code

=head2 SQL::Abstract::Distinction

This takes C<True>for a distinct row, or a C<Column::List> for specific rows that have to be distinct.

=head2 Window::Definition

Window definiton converts from a map taking the following keys, all optional:

=begin item1
Identifier(Cool) :$existing

This takes the name of an existing window
=end item1

=begin item1
Column::List(Any) :$partition-by

This lists expressions to partition the rows by, somewhat similar to a C<GROUP BY>.
=end item1

=begin item1
OrderBy(Any) :$order-by

The order within a frame.
=end item1

=begin item1
Boundary(Any) :$from

This argument defines the starting boundary of the frame. This can be any of:

=item2 'preceding'
=item2 :preceding($amount)
=item2 'current'
=item2 :following($amount)
=end item1

It defaults to 'preceding'.

=begin item1
Boundary(Any) :$to

This optional argument defines the ending boundary of the frame, This can be any of:

=item2 :preceding($amount)
=item2 'current'
=item2 :following($amount)
=item2 'following'

=end item1

=begin item1
Mode:D(Str) :$mode = Mode::Range

The mode of the frame takes one of C<'rows'>, C<'range'> or C<'groups'>, defaulting to C<'range'>.
=end item1

=begin item1
Exclusion(Str) :$exclude

The exclusion of the frame, it takes one of C<'current row'>, C<'group'>, C<'ties'> or C<'no others'> (the default).
=end item1

=begin code :lang<raku>

my Window::Definition $d = { :partition-by<foo bar>, :from<current> :to(:following(5)), :exclude<ties> }
# PARTITION BY foo, bar RANGE BETWEEN CURRENT ROW AND 5 FOLLOWING EXCLUDE TIES

=end code

=head2 Window::Clauses

This takes one or more pairs, with the names being windows names and the values taking window definition maps.

=begin code :lang<raku>

my Windows::Clauses $clauses =
    over5 => { :frame{ :preceding(5) } },
    foo => { :partition-by<foo bar>, :mode<range>, :from<current> :to(:following(5)), :exclude<ties> };
# WINDOW
#   over5 AS (ROWS 5 PRECEDING),
#   foo as (PARTITION BY foo, bar RANGE BETWEEN CURRENT ROW AND 5 FOLLOWING EXCLUDE TIES)

=end code

=head2 SQL::Abstract::Limit / SQL::Abstract::Offset

These both take either an C<Int> or an C<Expression>.

=head1 Capture expressions

Captures can be used in most places where Expressions can be used. They allow for SQL expressions that can't be encoded using simpler values.

There are two kinds of capture expressions. The first kind has one or more named arguments; the first will be used as literal SQL, the rest will be arguments for the literal. E.g. C<\'NOW()'>.

The second kind takes a single named argument that may or may not contain a value. Currently supported are:

=begin item1
Any :$bind

This represents a placeholder variable with the given value
=end item1

=begin item1
Bool :$default

This represents the C<DEFAULT> keyword
=end item1

=begin item1
Bool :$true

This represents the C<TRUE> keyword
=end item1

=begin item1
Bool :$false

This represents the C<FALSE> keyword
=end item1

=begin item1
Bool :$null

This represents the C<NULL> keyword
=end item1

=begin item1
Str :$ident

This represents an identifier (typically a column or table name)
=end item1

=begin item1
Bool :$star

This represents a C<*>.
=end item1

=begin item1
Any :$idents

This represents a list of identifiers
=end item1

=begin item1
Any :$columns

This represents a list of column expressions
=end item1

=begin item1
:op(@) ('not'|'+'|'-'|'~', Any $expr)

This represents a unary operator (C<NOT>, C<+>, C<->, C<~>).
=end item1

=begin item1
:op(@) ('like'|'not-like', Any $left-expr, Any $right-expr, Any $escape-expr)

This represents a C<LIKE> operator, e.h. C<column LIKE '%foo?' ESCAPE '\\'>
=end item1

=begin item1
:op(@) ('between'|'not-between', Any $column-expr, Any $left-expr, Any $right-expr)

This represents a C<BETWEEN> expression.
=end item1

=begin item1
:@and

This represents an AND expresssion. Typically the contents of this will be further capture expressions.
=end item1

=begin item1
:@or

This represents an OR expresssion. Typically the contents of this will be further capture expressions.
=end item1
=begin item1
:@op ('in'|'not-in', Any $left-expr, Capture $ (SQL::Abstract::Select(Map) :$select!))

This represents an <IN> expression with subquery. E.g. C<foo in (SELECT bar from baz where baz.id = table.id)>.
=end item1

=begin item1
:@op ('in'|'not-in', Any $left-expr, *@args)

This represents an <IN> expression with list. E.g. C<foo in (1, 2, 3)>.
=end item1

=begin item1
Select(Map) :$exists

This represents an C<EXISTS> expression. E.g. C<\(:exists{ :from<table>, columns<1>, :where{ :id(\(:ident<outer.id>)) }> for C<EXISTS(SELECT 1 FROM table WHERE id = outer.id>.
=end item1

=begin item1
Select(Map) :$not-exists

This is like :exists, but negated.
=end item1

=begin item1
:@op ("cast", Any $expression, Str $typename)

This is a <CAST> expression. E.g. C<CAST(columns AS INTEGER)>.
=end item1

=begin item1
:op(@) (Str $key, Any $left-expr, Any $right-expr)

Any binary operator applied to two expressions.
=end item1

=begin item1
Identifier(Any) :$current

This represents a C<CURRENT FOR cursor_name> clause, typically used in an C<UPDATE> or C<DELETE> statement.
=end item1

=begin item1
List :$row

This represents a row expression, e.g. C<(a, b, c)>.
=end item1

=begin item1
Function(Map) :$function

This represents a function call.
=end item1

=begin item1
Select(Map) :$select

This represents a subquery expression.
=end item1

=head1 Author

Leon Timmermans <fawaka@gmail.com>

=head1 Copyright and License

Copyright 2022 Leon Timmermans

This library is free software; you can redistribute it and/or modify it under the Artistic License 2.0.

=end pod


