The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Catmandu::Importer::LDAP;

use Catmandu::Sane;
use Catmandu::Util qw(:is);
use Carp qw(confess);
use Net::LDAP;
use Moo;

with 'Catmandu::Importer';

our $VERSION = '0.0104';

has host          => (is => 'ro', default => sub { 'ldap://127.0.0.1:389' });
has base          => (is => 'ro', predicate => 1);
has password      => (is => 'ro', predicate => 1);
has search_base   => (is => 'ro', predicate => 1);
has search_filter => (is => 'ro', predicate => 1);
has ldap          => (is => 'ro', lazy => 1, builder => '_build_ldap');
has attributes    => (
    is     => 'ro',
    coerce => sub {
        my $attrs = $_[0];
        if (is_string $attrs) {
            return { map { $_ => {} } split ',', $attrs };
        }
        if (is_array_ref $attrs) {
            return { map { $_ => {} } @$attrs };
        }
        if ($attrs) {
            for my $attr (keys %$attrs) {
                $attrs->{$attr} = {} unless ref $attrs->{$attr};
            };
        }
        $attrs;
    },
);

sub _build_ldap {
    my $self = $_[0];
    my $ldap = Net::LDAP->new($self->host, raw => qr/;binary/) || confess $@;
    my $bind = $self->has_base
        ? $self->has_password
            ? $ldap->bind($self->base, password => $self->password)
            : $ldap->bind($self->base)
        : $ldap->bind;

    if ($bind->code != 0) {
        $self->log->error($bind->error);
        return undef;
    }
    $ldap;
}

sub _new_search {
    my $self = $_[0];
    my %args;
    $args{base}   = $self->search_base   if $self->has_search_base;
    $args{filter} = $self->search_filter if $self->has_search_filter;
    if (my $attrs = $self->attributes) {
        $args{attrs} = [keys %$attrs];
    }
    my $search = $self->ldap->search(%args);
    if ($search->code != 0) {
        $self->log->error($search->error);
    }
    $search;
}

sub generator {
    my $self = $_[0];
    sub {
        state $search = $self->_new_search;
        my $entry = $search->shift_entry // return;
        my $data = {};
        if (my $attrs = $self->attributes) {
            for my $attr (keys %$attrs) {
                my $config = $attrs->{$attr};
                my $val = $entry->get_value($attr, asref => $config->{array}) // next;
                $data->{$config->{as} // $attr} = $config->{array} ? [@$val] : $val;
            }
        } else {
            for my $attr ($entry->attributes) {
                my $val = $entry->get_value($attr, asref => 1);
                $data->{$attr} = [@$val];
            }
        }
        $data;
    };
}

=head1 NAME

Catmandu::Importer::LDAP - Package that imports LDAP directories

=head1 SYNOPSIS

    # From the command line

    # Anonymous bind to find all 'Patrick's
    $ catmandu convert LDAP \
            --host ldaps://ldaps.ugent.be \
            --search-filter '(givenName=Patrick)' \
            --search-base 'dc=ugent, dc=be' to YAML

    # From Perl

    use Catmandu;

    my $importer = Catmandu->importer('LDAP',
                         host          => 'ldaps://ldaps.ugent.be' ,
                         search_filter => '(givenName=Patrick)' ,
                         search_base   => 'dc=ugent, dc=be'
                    );

    my $exporter = Catmandu->exporter('YAML');

    $exporter->add_many($importer);

    $exporter->commit;

=head1 CONFIGURATION

=over

=item host

The LDAP host to connect to

=item base

The base to bind to (if not specified it is an anonymous bind)

=item password

The password needed for the bind

=item search_base

The DN that is the base object entry relative to which the search is to be performed.

=item search_filter

One or more search filters. E.g.

   (givenName=Patrick)    # search Patrick
   (&(givenName=Patrick)(postalCode=9000))  # search Patrick AND postalcode=9000
   (|)(givenName=Patrick)(postalCode=9000)) # search Patrick OR postcalcode=9000

=back

=head1 METHODS

Every Catmandu::Importer is a Catmandu::Iterable all its methods are inherited.
The methods are not idempotent: LDAP streams can only be read once.

=head1 SEE ALSO

L<Catmandu> ,
L<Catmandu::Importer> ,
L<Catmandu::Iterable>

=cut

1;