#! /usr/bin/perl -w # Copyright (c) 2010-2011 Koos van den Hout, Henk P. Penning. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY Koos van den Hout, Henk P. Penning, ``AS IS'' # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, # THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Koos van den Hout or Henk P. # Penning OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED # TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # The views and conclusions contained in the software and documentation are # those of the authors and should not be interpreted as representing official # policies, either expressed or implied, of Koos van den Hout, Henk P. Penning. # ----------------------------------------------------------------------------- # "Simplified BSD License" or "FreeBSD License" # http://en.wikipedia.org/wiki/BSD_licenses use strict ; package Blib ; ############################################################## use Exporter ; use Carp ; our ( @EXPORT, @EXPORT_OK, @ISA ) ; BEGIN { require Exporter ; @EXPORT = qw() ; @EXPORT_OK = qw(mk_method mk_methods new_pack unref) ; @ISA = qw(Exporter) } our %LOGLEVELS = ( quiet => 0 , terse => 1 , verbose => 2 , debug => 3 ) ; our $MEEK = {} ; sub unref ; sub MEEK { $MEEK } ; sub MEEK_incr { my $pack = unref shift ; $MEEK -> { $pack } { incr } ++ ; } sub MEEK_decr { my $pack = unref shift ; $MEEK -> { $pack } { decr } ++ ; } sub MEEK_dump { join '', map { my $incr = $MEEK -> { $_ } { incr } || 0 ; my $decr = $MEEK -> { $_ } { decr } || 0 ; sprintf "%6s %6s %s\n", $incr, $incr - $decr, $_ ; } sort keys %$MEEK ; } # DESTROY { my $self = shift ; MEEK_decr ( $self ) ; } sub unref { my $self = shift ; ref $self or $self ; } sub _bless { my $val = shift ; my $pac = shift ; # MEEK_incr ( $pac ) ; bless $val, $pac ; } sub _is_loglevel { my $self = shift ; my $cand = shift ; exists $LOGLEVELS { $cand } ; } sub _loglevels { my $self = shift ; sort { $LOGLEVELS { $a } <=> $LOGLEVELS { $b } ; } keys %LOGLEVELS ; } sub _pr { my @args = @_ ; if ( Blib::Mods -> can ( 'print' ) ) { Blib::Mods::print ( @args ) ; } else { print @args ; } } sub _pf { my @args = @_ ; if ( Blib::Mods -> can ( 'printf' ) ) { Blib::Mods::printf ( @args ) ; } else { printf @args ; } } sub _pr_hash { die "_pr_hash : need 2 args" unless @_ == 2 ; my $tag = shift ; my $hash = shift ; _pf "%s :\n\{%s\n\}\n", $tag , join "\n, ", map { my $val = $hash -> { $_ } ; sprintf "%s=>%s", $_, ( defined $val ? $val : 'UNDEF' ) } sort keys %$hash ; } my %Opts = () ; sub Opts { my $self = shift ; $Opts { $self } = { q => 0 , t => 0 , v => 0 , d => 0 , self => $self } unless exists $Opts { $self } ; $Opts { $self } ; } sub dmp_opt { my $hash = shift ; sprintf "{%s}\n" , join ' , ' , map { "$_=>$hash->{$_}" } ( 'self', sort grep { $_ ne 'self' } keys %{ $hash } ) ; } my $level = 0 ; sub lvl { my $lvl = shift ; $level = $lvl if defined $lvl ; $lvl ; } sub xx1 { Blib::_pr ( @_ ) if $level > 0 ; } sub xx2 { Blib::_pr ( @_ ) if $level > 1 ; } sub _opt { my $self = shift ; my ( $recurse, $key, $val ) = @_ ; unless ( $self ) { xx1 "self empty ; result=0" ; return 0 ; } my $opts = Opts $self ; my $parent = $self -> parent ; my $v = ( defined $val ? $val : '' ) ; xx1 "self ($self) key ($key) val ($v) recurse ($recurse)\n" ; xx1 dmp_opt $opts ; unless ( exists $opts -> { $key } ) { Carp::confess "_opt : unknown option '$key'\n" ; } elsif ( defined $val ) { Carp::confess "_opt : bad arg ($val)" if $val eq 'Blib' ; $opts -> { $key } = $val ; xx1 "set key ($key) val ($val)\n" ; } $val = $opts -> { $key } ; xx1 "key ($key) => val ($val)\n" ; xx1 dmp_opt ( $opts ) ; my $res = 0 ; if ( ! defined $val ) { $res = 0 ; xx2 "undef\n" ; } elsif ( $val ) { $res = $val % 2 ; xx2 sprintf "result ($val)\n" ; } elsif ( $recurse and $parent ) { xx1 "recurse\n" ; $res = $parent -> _opt ( $recurse, $key, undef ) ; } else { xx2 "else ($val)\n" ; } xx1 "=> self ($self) key ($key) res ($res)\n" ; $res ; } sub _dqv { my $self = shift ; xx1 "\n" ; $self -> _opt ( @_ ) ; } sub quiet { my $self = shift ; $self -> _dqv ( 1, 'q', shift ) ; } sub quiet_me { my $self = shift ; $self -> _dqv ( 0, 'q', shift ) ; } sub terse { my $self = shift ; $self -> _dqv ( 1, 't', shift ) ; } sub terse_me { my $self = shift ; $self -> _dqv ( 0, 't', shift ) ; } sub verbose { my $self = shift ; $self -> _dqv ( 1, 'v', shift ) ; } sub verbose_me { my $self = shift ; $self -> _dqv ( 0, 'v', shift ) ; } sub debug { my $self = shift ; $self -> _dqv ( 1, 'd', shift ) ; } sub debug_me { my $self = shift ; $self -> _dqv ( 0, 'd', shift ) ; } sub set_opts { my $self = shift ; my %opts = @_ ; if ( $opts{d} ) { $self -> set_loglevel ( 'debug' ) ; } elsif ( $opts{v} ) { $self -> set_loglevel ( 'verbose' ) ; } elsif ( $opts{q} ) { $self -> set_loglevel ( 'quiet' ) ; } else { $self -> set_loglevel ( 'terse' ) ; } } # parent '' is '' ; the parent of an object instance is it's class sub parent { my $self = shift ; if ( ref $self ) { ref $self ; } elsif ( $self =~ /^\w*$/ ) { '' ; } elsif ( $self =~ /::\w+$/ ) { $` ; } else { Carp::confess "parent : bad arg ($self)" ; } } sub set_loglevel { my $self = shift ; my $level = shift ; my $where = shift ; if ( Blib -> _is_loglevel ( $level ) ) { my $mark = $LOGLEVELS { $level } ; for my $llvl ( keys %LOGLEVELS ) { $self -> $llvl ( $LOGLEVELS { $llvl } <= $mark ) ; } Blib::_pf ( "set loglevel $self '%s' (%s)\n" , $level , ( $where || 'somewhere' ) ) if $where or $self -> debug ; } else { Carp::confess "set_log_level : bad level ($level)" ; } } sub new { my $self = shift ; my $name = shift ; if ( defined $name ) { $self = "${self}::${name}" ; Carp::confess "Blib::new : bad name ($name)" if $name =~ /^Dbs|Tab|Rec|Mods$/ ; new_pack ( 'Blib', 'Mods' ) ; new_pack ( 'Blib', $name ) ; new_pack ( $self ) ; } _bless {}, $self ; } sub make { my $self = shift ; my @args = @_ ; $self -> new -> init ( @args ) ; } sub add_ISA { my $self = shift ; my $pack = shift ; my $isa = sprintf 'push @%s::ISA, "%s" ;', $self, $pack ; Blib::_pr ( "\n# multiple inheritance for $self\n$isa\n\n" ) if Blib -> debug ; my $res = eval "$isa 1 ;" ; Carp::confess "add_isa : $@" unless defined $res ; $res ; } my %_dumped ; my %_nodump ; my $_verbos ; sub _dmpval { my $self = shift ; my $val = shift ; my $depth = shift ; my $sep = ' ' x ( 2 * $depth ) ; my $dmp ; if ( ! defined $val ) { $dmp = '' ; } elsif ( ref $val and eval { $val -> can ( 'dmp' ) } ) { if ( exists $_dumped { $val } ) { $dmp = "recursed ($val)" ; } else { $_dumped { $val } ++ ; $dmp = $val -> _dmp ( $depth + 1 ) ; } ; } elsif ( ref $val eq 'ARRAY' ) { my $can = ( @$val and eval { $val -> [ 0 ] -> can ( 'dmp' ) } ) ; $dmp = ( scalar @$val ? sprintf "\n${sep} [ %s\n$sep ]" , join "\n$sep , " , ( $can ? map { $_ -> _dmp ( $depth + 1 ) } @$val : @$val ) : '[]' ) ; } elsif ( ref $val eq 'HASH' ) { $dmp = ( scalar keys %$val ? sprintf "\n${sep} { %s\n$sep }" , join "\n$sep , " , map { sprintf "%s => %s", $_, $val -> { $_ } } sort keys %$val : '{}' ) ; } elsif ( ! ref $val and $val eq '' ) { $dmp = '' ; } else { $val =~ s/(ARRAY|HASH)\(\w+\)/$1/ ; $dmp = $val ; } $dmp ; } sub _dmp { my $self = shift ; my $depth = shift ; my $ref = ref $self ; %_dumped = () if $depth == 0 ; my $sep = ' ' x ( 2 * $depth ) ; my $slf = $depth ? '' : 'self ' ; my $res = sprintf "%sis a '%s'\n", $slf, $ref ; $res .= sprintf "${sep}ISA (%s)\n", eval '@' . "${ref}::ISA" ; my $w = 0 ; if ( ref $self and $self =~ /HASH/ ) { for my $key ( keys %$self ) { $w = length $key if length $key > $w ; } for my $key ( sort keys %$self ) { my $val = $self -> { $key } ; my $dmp ; if ( $_nodump { $key } ) { next unless $_verbos ; $dmp = " $val" ; } else { $dmp = $self -> _dmpval ( $val, $depth ) ; } my $nl = ( "\n" eq substr ( $dmp, -1, 1 ) ? '' : "\n" ) ; $res .= sprintf "%s%-${w}s : %s%s" , $sep , $key , $dmp, $nl ; } } elsif ( ref $self and $self =~ /ARRAY/ ) { my $can = ( @$self and eval { $self -> [ 0 ] -> can ( 'dmp' ) } ) ; $res .= ( scalar @$self ? sprintf "${sep} [ %s\n$sep ]" , join "\n$sep , " , ( $can ? map { $_ -> _dmp ( $depth + 1 ) } @$self : @$self ) : '[]' ) ; } $res ; } sub dmp { my $self = shift ; my %opts = ( -excl => [ qw(_cafe _tabl _base) ] , -incl => [] , -verb => 1 , @_ ) ; %_dumped = () ; %_nodump = () ; $_verbos = $opts { -verb } ; for my $key ( @{ $opts { -excl } } ) { $_nodump { $key } ++ ; } for my $key ( @{ $opts { -incl } } ) { delete $_nodump { $key } ; } Blib::_pf ( "excluding [%s]\n", join ',', sort keys %_nodump ) if $_verbos ; Blib::_pr ( $self -> _dmp ( 0 ) ) ; } sub _getset { my $self = shift ; my $attr = shift ; Carp::confess "self not ref ($self)" unless ref $self ; if ( @_ ) { $self -> { $attr } = shift ; } Carp::confess "no attr '$attr' ($self)" unless exists $self -> { $attr } ; my $res = $self -> { $attr } ; ( wantarray and ref $res eq 'ARRAY' ) ? @$res : $res ; } sub mk_typ { my $self = shift ; my $d = shift ; my $type = ref $self || $self ; die "mk_typ : bad type ($type)" unless $type =~ /Blib::(Dbs|Tab|Col|Rec)(::|$)?/ ; my $s = ( split /::/, $type ) [ 1 ] ; $type =~ s/$s/$d/ ; $type ; } sub mk_method { my $self = shift ; my $attr = shift ; sprintf 'sub %s { my $self = shift ; $self -> _getset ( "%s", @_ ) ; }' , $attr, $attr ; } sub mk_methods { my $self = shift ; join "\n", map { Blib -> mk_method ( $_ ) ; } @_ ; } sub new_pack { my $type = shift ; my $name = shift ; my @isa = split /::/, $type ; my $isa = shift @isa ; $isa .= '::' . shift @isa if @isa ; my $new_typ = $type ; my $new_isa = $isa ; if ( defined $name ) { $new_typ = "${type}::${name}" ; $new_isa = $type ; } my $fmt = <<'PACK' ; { package %s ; } { @%s::ISA = qw(%s) ; } PACK my $pack = sprintf $fmt, $new_typ, $new_typ, $new_isa ; Blib::_pr $pack if Blib -> debug ; my $res = eval "$pack ; 1 ;" ; Carp::confess "new_pack : $@" unless defined $res ; $pack ; } sub new_method { my $pack = shift ; my $attr = shift ; my $fmt = "{ package %s ; eval Blib -> mk_method ( '%s' ) ; }\n" ; my $mth = sprintf $fmt, $pack, $attr, $attr ; Blib::_pr ( $mth ) if Blib -> debug ; my $res = eval "$mth ; 1 ;" ; Carp::confess "new_method : $@" unless defined $res ; } ### JSON #################################################################### package Blib::JSON ; our @ISA ; BEGIN { @ISA = qw(Blib) ; } eval Blib -> mk_methods ( qw(__) ) ; my %name4key ; sub add_name4key { my $self = shift ; my %opts = @_ ; for my $key ( keys %opts ) { $name4key { $key } = $opts { $key } ; } } sub name4key { my $self = shift ; my $x = shift ; $name4key { $x } || $x ; } my %name4row ; sub add_name4row { my $self = shift ; my %opts = @_ ; for my $key ( keys %opts ) { $name4row { $key } = $opts { $key } ; } } sub name4row { my $self = shift ; my $x = shift ; my $n = $x ; $n =~ s/=.*// ; $name4row { $n } ; } sub make { my $self = shift ; my %opts = @_ ; my $name = $opts { -name } ; $self -> new ( $name ) -> init ( %opts ) ; } sub init { my $self = shift ; my %opts = @_ ; $self -> __ ( undef ) ; for my $opt ( keys %opts ) { $self -> $opt ( $opts { $opt } ) ; } $self ; } sub add { my $self = shift ; my $type = Blib::unref $self ; my %opts = @_ ; my $name = $opts { -name } ; my $kind = $opts { -kind } ; my $tsub = "${type}::$name" ; Blib::new_method ( $type, $name ) ; Blib::new_pack ( $tsub ) if $kind ; my $res = Blib::_bless {}, $tsub ; if ( ref $self ) { $self -> { $name } = ( $kind ? $res : 'scalar' ) ; } $res ; } sub mk_sub_model ; sub mk_sub_model { my $self = shift ; my $base = shift ; my $name = shift ; my $json = shift ; my $kind = ref $json ; my $type = $base -> add ( -name => $name , -kind => $kind ) ; if ( $kind eq 'HASH' ) { for my $key ( sort keys %$json ) { my $name = Blib::JSON -> name4key ( $key ) ; $self -> mk_sub_model ( $type, $name, $json -> { $key } ) ; } } elsif ( $kind eq 'ARRAY' and @$json ) { my $elem = $json -> [ 0 ] ; my $name = Blib::JSON -> name4row ( $type ) ; die "no name for $type" unless $name ; $self -> mk_sub_model ( $type, $name, $elem ) ; } $type ; } sub mk_model { my $self = shift ; my $name = shift ; my $json = shift ; $self -> mk_sub_model ( 'Blib::JSON', $name, $json ) ; } sub bless { my $type = shift ; my $json = shift ; my $kind = ref $json ; Blib::_bless ( $json, Blib::unref $type ) ; if ( $kind eq 'HASH' ) { for my $key ( sort keys %$json ) { my $name = Blib::JSON -> name4key ( $key ) ; if ( $name ne $key ) { $json -> { $name } = $json -> { $key } ; delete $json -> { $key } ; } if ( ref $type -> $name ) { $type -> $name -> bless ( $json -> { $name } ) ; } } } elsif ( $kind eq 'ARRAY' and @$json and ref ( $json -> [ 0 ] ) ) { my $name = Blib::JSON -> name4row ( $type ) || 'elem' ; for my $elem ( @$json ) { $type -> $name -> bless ( $elem ) ; } } $json ; } 1 ; __END__ =pod =head1 NAME =head1 SYNOPSIS use Blib ; $base = Blib::Dbs -> make ( 'Foo' ) ; # symbolic name for your database $base -> connect ( "dbi:Pg:dbname=...;host=...", 'user', 'pswd' ) ; $tabl = $base -> add_tab ( -name => 'zoo' # table name , -pkey => 'key' # primary key , -tabl => 'tab_zoo' # real table name, default -name ) ; $col = $tabl -> add_col ( -name => 'bar' , -type => 'int' # or text, bool, serial , -uniq => 1 # values are uniq , -null => 1 # values may be NULL ) ; $zoo = $base -> zoo ; # yields table 'zoo' $bar = $zoo -> bar ; # yields column 'bar' $val = $rec > bar ; # yields value in column 'bar' ; $rec = $tabl -> select1 ( -where => "bar = 'some value'" ) ; =head1 EXAMPLE Let's start with an example and leave the details for later. Suppose we want to model a school where there are teachers, pupils and courses. We have a database I with tables I, I and I and many-to-many tables I (which teacher teaches what courses) and I (which pupil follows what courses). Suppose the Blib::Dbs::School object is set up with all the tables and columns added. Now we want to add some functions to the models # create Blib::Dbs::School etc ; define_tables is done elsewhere my $school = Blib::Dbs -> make ( 'School' ) -> define_tables ; # add to the model for 'a teacher' Package Blib::Rec::School::teacher ; # the teacher's name # uses the field-names as methods sub name { my $self = shift ; $self -> last_name . ', ' . $self -> initials ; } # the courses a teacher gives # a Blib::Rec::School::teacher knows the database it belongs to, # It finds table 'teaches' as $self -> _base -> teaches sub courses { my $self = shift ; my $id = $self -> id ; my $course_ids = $self -> _name -> _base -> teaches -> _make_sql ( -fields => 'course_id', -where => "teacher_id = '$id' ) ; $self -> _base -> courses -> select ( -where => "id in ( $course_ids )" ) ; } # add to the model for 'a course' Package Blib::Rec::School::course ; # the pupils following a course sub pupils { my $self = shift ; my $id = $self -> id ; my $pupil_ids = $self -> _base -> follows -> _make_sql ( -fields => 'pupil_id', -where => "course_id = '$id' ) ; $self -> _base -> courses -> select ( -where => "id in ( $pupil_ids )" , -order => 'name' ) ; } # back to the teacher's model Package Blib::Rec::School::teacher ; # a teacher's pupils # uses Blib::Tab -> _make_sql to formulate a query for a specific table sub pupils { my $self = shift ; my $id = $self -> id ; # the query that yields her course_id's my $course_ids = $self -> _base -> teaches -> _make_sql ( -fields => 'course_id', -where => "teacher_id = '$id' ) ; # the query that yields the unique pupil_id's my $pupil_ids = $self -> _base -> follows -> make _sql ( -fields => 'distinct ( pupil_id )' , -where => "course_id in ( $course_ids ) ) ; # select the pupils my $pupils = $self -> _base -> pupils -> select ( -where => "id in ( $pupil_ids )" , -order => 'name' ) ; } # or a teacher's pupils, with a join sub pupils { my $self = shift ; $self -> _base -> pupils -> select ( -fields => 'distinct pupils.*' , -from => "gives, follows, pupils" , -where => "gives.teacher_id = '$id' " . "and gives.course_id = follows.course_id " . "and follows.pupil_id = pupil.pupil_id" , -order => 'name' ) ; } # or a teacher's pupils, with plain perl # for each teacher's course, find the pupils ; hash for uniqueness. sub pupils { my $self = shift ; my %pupils = () ; for my $course ( $self -> courses ) { my @pupils = $course -> pupils ; @pupils { @pupils } = @pupils ; } sort { $a -> name cmp $b -> name } keys %pupils ; } The point here is that we can select the required objects without writing too much, and build up the model in small steps. =head1 Blib::Dbs class methods =over 4 =item $base = new ( I ) Function I creates class Blib::Dbs::I and yields an object of that class. Blib::Dbs::I is a subclass of Blib::Dbs. It is the basis of the model of your database. =head1 Blib::Dbs object methods =item $base = add_tab ( I ) =over 4 =item * option -name => table-name =item * option -pkey => column-name =item * option -tags => list of column-names The I are used to create a sensible tag for a table record ; The I of the record is a space-separated list of the field-values as specified by the list, as in C as opposed to C. Default C<[]>. =item * option -tabl => real table name Set the real table names when they are user-configurable, for instance prefixed with some fixed string. Default C<-name>. =back If the table is the first table added to the Blib::Dbs object, function C also creates classes Blib::Tab::I (a subclass of Blib::Tab) and Blib::Rec::I (a subclass of Blib::Rec). These classes can be used later to implement functionality common to all tables and all records of the database. =back =cut