package Rose::DB::Registry;

use strict;

use Carp();

use Rose::DB::Registry::Entry;

use Rose::Object;
our @ISA = qw(Rose::Object);

our $VERSION = '0.728';

our $Debug = 0;

#
# Object data
#

use Rose::Object::MakeMethods::Generic
(
  'scalar' =>
  [
    qw(error)
  ],

  'scalar --get_set_init' =>
  [
    'hash',
    'parent',
  ],
);

#
# Object methods
#

sub init_hash   { {} }
sub init_parent { 'Rose::DB' }

sub add_entries
{
  my($self) = shift;

  # Smuggle parent in with an otherwise nonsensical arrayref arg
  my $parent = shift->[0]  if(ref $_[0] eq 'ARRAY');
  $parent ||= $self->parent;

  my $entries = $self->hash;

  my @added;

  foreach my $item (@_)
  {
    my($domain, $type, $entry);

    if(ref $item eq 'HASH')
    {
      if($entry = delete $item->{'entry'})
      {
        $domain = delete $item->{'domain'};
        $type   = delete $item->{'type'};

        if(keys(%$item))
        {
          Carp::croak "If an 'entry' parameter is passed, no other ",
                      "parameters (other than 'domain' and 'type') ",
                      "may be passed";
        }
      }
      else
      {
        $entry = Rose::DB::Registry::Entry->new(%$item);
      }
    }
    elsif(ref $item && $item->isa('Rose::DB::Registry::Entry'))
    {
      $entry = $item;
    }
    else { Carp::croak "Don't know how to add registry entry '$item'" }

    $domain = $entry->domain  unless(defined $domain);
    $type   = $entry->type    unless(defined $type);

    unless(defined $domain)
    {
      $domain = $parent->default_domain;
      $entry->domain($domain);
    }

    unless(defined $type)
    {
      $type = $parent->default_type;
      $entry->type($type);
    }

    Carp::confess "$parent - Missing domain for registry entry: domain '$domain', type '$type'"
      unless(defined $domain);

    Carp::confess "$parent - Missing type for registry entry: domain '$domain', type '$type'"
      unless(defined $type);

    Carp::confess "$parent - Missing driver for registry entry: domain '$domain', type '$type'"
      unless(defined $entry->driver);

    $entries->{$domain}{$type} = $entry;    
    push(@added, $entry);
  }

  return wantarray ? @added : \@added;
}

sub add_entry
{
  my($self) = shift;

  # Smuggle parent in with an otherwise nonsensical arrayref arg
  my $parent = shift  if(ref $_[0] eq 'ARRAY');

  if(@_ == 1 || (ref $_[0] && $_[0]->isa('Rose::DB::Registry::Entry')))
  {
    return ($self->add_entries(($parent ? $parent : ()), @_))[0];
  }

  return ($self->add_entries(($parent ? $parent : ()), { @_ }))[0];
}

sub entry_exists
{
  my($self, %args) = @_;

  Carp::croak "Missing required 'type' argument"
    unless(defined $args{'type'});

  Carp::croak "Missing required 'domain' argument"
    unless(defined $args{'domain'});

  return exists $self->hash->{$args{'domain'}}{$args{'type'}};
}

sub delete_entry
{
  my($self, %args) = @_;
  return undef  unless($self->entry_exists(%args));
  return delete $self->hash->{$args{'domain'}}{$args{'type'}};
}

sub entry
{
  my($self, %args) = @_;
  return undef  unless($self->entry_exists(%args));
  return $self->hash->{$args{'domain'}}{$args{'type'}};
}

sub delete_domain
{
  my($self, $domain) = @_;
  my $entries = $self->hash;
  delete $entries->{$domain};
}

sub registered_types
{
  my($self, $domain) = @_;
  my @types = sort keys %{ $self->hash->{$domain} || {} };
  return wantarray ? @types : \@types;
}

sub registered_domains
{  
  my @domains = sort keys %{ shift->hash };
  return wantarray ? @domains : \@domains;
}

sub dump
{
  my($self) = shift;

  my $entries = $self->hash;
  my %reg;

  foreach my $domain ($self->registered_domains)
  {
    foreach my $type ($self->registered_types($domain))
    {
      $reg{$domain}{$type} = $entries->{$domain}{$type}->dump;
    }
  }

  return \%reg;
}

1;

__END__

=head1 NAME

Rose::DB::Registry - Data source registry.

=head1 SYNOPSIS

  use Rose::DB::Registry;

  $registry = Rose::DB::Registry->new;

  $registry->add_entry(
    domain   => 'development',
    type     => 'main',
    driver   => 'Pg',
    database => 'dev_db',
    host     => 'localhost',
    username => 'devuser',
    password => 'mysecret',
    server_time_zone => 'UTC');

  $entry = Rose::DB::Registry::Entry->new(
    domain   => 'production',
    type     => 'main',
    driver   => 'Pg',
    database => 'big_db',
    host     => 'dbserver.acme.com',
    username => 'dbadmin',
    password => 'prodsecret',
    server_time_zone => 'UTC');

  $registry->add_entry($entry);

  $entry = $registry->entry(domain => 'development', type => 'main');

  $registry->entry_exists(domain => 'foo', type => 'bar'); # false

  $registry->delete_entry(domain => 'development', type => 'main');

  ...

=head1 DESCRIPTION

L<Rose::DB::Registry> objects manage information about L<Rose::DB> data sources.  Each data source has a corresponding L<Rose::DB::Registry::Entry> object that contains its information.  The registry entries are organized in a two-level namespace based on a "domain" and a "type."  See the L<Rose::DB> documentation for more information on data source domains and types.

L<Rose::DB::Registry> inherits from, and follows the conventions of, L<Rose::Object>.  See the L<Rose::Object> documentation for more information.

=head1 CONSTRUCTOR

=over 4

=item B<new PARAMS>

Constructs a L<Rose::DB::Registry> object based on PARAMS, where PARAMS are
name/value pairs.  Any object method is a valid parameter name.

=back

=head1 OBJECT METHODS

=over 4

=item B<add_entries ENTRY1 [, ENTRY2, ...]>

Add registry entries.  Each ENTRY must be either a L<Rose::DB::Registry::Entry>-derived object or  reference to a hash of name/value pairs.  The name/value pairs must be valid arguments for L<Rose::DB::Registry::Entry>'s constructor.

Each ENTRY must have a defined domain and type, either in the L<Rose::DB::Registry::Entry>-derived object or in the name/value pairs.  A fatal error will occur if these values are not defined.

If a registry entry for the specified domain and type already exists, then the new entry will overwrite it.  If you want to know beforehand whether or not an entry exists under a specific domain and type, use the L<entry_exists|/entry_exists> method.

Returns a list (in list context) or reference to an array (in scalar context) of L<Rose::DB::Registry::Entry> objects added.

=item B<add_entry ENTRY>

Add a registry entry.  ENTRY must be either a L<Rose::DB::Registry::Entry>-derived object or a list of name/value pairs.  The name/value pairs must be valid arguments for L<Rose::DB::Registry::Entry>'s constructor.

The ENTRY must have a defined domain and type, either in the L<Rose::DB::Registry::Entry>-derived object or in the name/value pairs.  A fatal error will occur if these values are not defined.

If a registry entry for the specified domain and type already exists, then the new entry will overwrite it.  If you want to know beforehand whether or not an entry exists under a specific domain and type, use the L<entry_exists|/entry_exists> method.

Returns the L<Rose::DB::Registry::Entry> object added.

=item B<dump>

Returns a reference to a hash containing information about all registered data sources.  The hash is structured like this:

    {
      domain1 =>
      {
        type1 =>
        {
          # Rose::DB::Registry::Entry attributes
          # generated by its dump() method
          driver   => ...,
          database => ...,
          host     => ...,
          ...
        },

        type2 =>
        {
          ...
        },
        ...
      },

      domain2 =>
      {
        ...
      },

      ...
    }

All the registry entry attribute values are copies, not the actual values.

=item B<delete_domain DOMAIN>

Delete an entire domain, including all the registry entries under that domain.

=item B<delete_entry PARAMS>

Delete the registry entry specified by PARAMS, where PARAMS must be name/value pairs with defined values for C<domain> and C<type>.  A fatal error will occur if either one is missing or undefined.

If the specified entry does not exist, undef is returned.  Otherwise, the deleted entry is returned.

=item B<entry PARAMS>

Get the registry entry specified by PARAMS, where PARAMS must be name/value pairs with defined values for C<domain> and C<type>.  A fatal error will occur if either one is missing or undefined.  If the specified entry does not exist, undef is returned.

=item B<entry_exists PARAMS>

Returns true if the registry entry specified by PARAMS exists, false otherwise.  PARAMS must be name/value pairs with defined values for C<domain> and C<type>.  A fatal error will occur if either one is missing or undefined.

=item B<registered_types DOMAIN>

Returns a list (in list context) or reference to an array (in scalar context) of the names of all registered types under the domain named DOMAIN.

=item B<registered_domains>

Returns a list (in list context) or reference to an array (in scalar context) of the names of all registered domains.

=back

=head1 AUTHOR

John C. Siracusa (siracusa@gmail.com)

=head1 LICENSE

Copyright (c) 2010 by John C. Siracusa.  All rights reserved.  This program is
free software; you can redistribute it and/or modify it under the same terms
as Perl itself.