File Coverage

blib/lib/RapidApp/TableSpec/ColSpec.pm
Criterion Covered Total %
statement 30 31 96.7
branch n/a
condition n/a
subroutine 12 13 92.3
pod 0 8 0.0
total 42 52 80.7


line stmt bran cond sub pod time code
1             package RapidApp::TableSpec::ColSpec;
2 5     5   30 use strict;
  5         11  
  5         150  
3 5     5   26 use Moose;
  5         11  
  5         26  
4 5     5   26689 use Moose::Util::TypeConstraints;
  5         13  
  5         33  
5              
6 5     5   8930 use RapidApp::Util qw(:all);
  5         13  
  5         2168  
7              
8             =head1 ColSpec format 'include_colspec'
9              
10             The include_colspec attribute defines joins and columns to include. It consists
11             of a list of "ColSpecs"
12              
13             The ColSpec format is a string format consisting of consists of 2 parts: an
14             optional 'relspec' followed by a 'colspec'. The last dot "." in the string separates
15             the relspec on the left from the colspec on the right. A string without periods
16             has no (or an empty '') relspec.
17              
18             The relspec is a chain of relationship names delimited by dots. These must be exact
19             relnames in the correct order. These are used to create the base DBIC join attr. For
20             example, this relspec (to the left of .*):
21              
22             object.owner.contact.*
23            
24             Would become this join:
25              
26             { object => { owner => 'contact' } }
27            
28             Multple overlapping rels are collapsed in an inteligent manner. For example, this:
29              
30             object.owner.contact.*
31             object.owner.notes.*
32            
33             Gets collapsed into this join:
34              
35             { object => { owner => [ 'contact', 'notes' ] } }
36            
37             The colspec to the right of the last dot "." is a glob pattern match string to identify
38             which columns of that last relationship to include. Standard simple glob wildcards * ? [ ]
39             are supported (this is powered by the Text::Glob module. ColSpecs with no relspec apply to
40             the base table/class. If no base colspecs are defined, '*' is assumed, which will include
41             all columns of the base table (but not of any related tables).
42              
43             Note that this ColSpec:
44              
45             object.owner.contact
46            
47             Would join { object => 'owner' } and include one column named 'contact' within the owner table.
48              
49             This ColSpec, on the other hand:
50              
51             object.owner.contact.*
52            
53             Would join { object => { owner => 'contact' } } and include all columns within the contact table.
54              
55             The ! chacter can exclude instead of include. It can only be at the start of the line, and it will
56             cause the colspec to exclude columns that match the pattern. For the purposes of joining, ! ColSpecs
57             are ignored.
58              
59             =head1 EXAMPLE ColSpecs:
60              
61             'name',
62             '!id',
63             '*',
64             '!*',
65             'project.*',
66             'user.*',
67             'contact.notes.owner.foo*',
68             'contact.notes.owner.foo.sd',
69             'project.dist1.rsm.object.*_ts',
70             'relation.column',
71             'owner.*',
72             '!owner.*_*',
73              
74             =cut
75              
76              
77 5     5   1098 use Type::Tiny;
  5         25419  
  5         5716  
78             my $TYPE_ColSpecStr = Type::Tiny->new(
79             name => "ColSpecStr",
80             constraint => sub {
81             /\s+/ and warn "ColSpec '$_' is invalid because it contains whitespace" and return 0;
82             #/[A-Z]+/ and warn "ColSpec '$_' is invalid because it contains upper case characters" and return 0;
83             /([^\#a-zA-Z0-9\-\_\.\!\*\?\[\]\{\}\:])/ and warn "ColSpec '$_' contains invalid characters ('$1')." and return 0;
84             /^\./ and warn "ColSpec '$_' is invalid: \".\" cannot be the first character" and return 0;
85             /\.$/ and warn "ColSpec '$_' is invalid: \".\" cannot be the last character (did you mean '$_*' ?)" and return 0;
86              
87             $_ =~ s/^\#//;
88             /\#/ and warn "ColSpec '$_' is invalid: # (comment) character may only be supplied at the begining of the string." and return 0;
89              
90             $_ =~ s/^\!//;
91             /\!/ and warn "ColSpec '$_' is invalid: ! (not) character may only be supplied at the begining of the string." and return 0;
92              
93             return 1;
94             },
95             message => sub { "$_ not a ColSpecStr (see previous warnings)" }
96             );
97 0     0 0 0 sub ColSpecStr { $TYPE_ColSpecStr }
98              
99             subtype 'ColSpecStr', as 'Str', where { $TYPE_ColSpecStr->constraint->(@_) };
100              
101             has 'colspecs', is => 'ro', isa => 'ArrayRef[ColSpecStr]', required => 1;
102 2986     2986 0 3472 sub all_colspecs { uniq( @{(shift)->colspecs} ) }
  2986         66838  
103             sub add_colspecs { push @{(shift)->colspecs}, @_ }
104              
105              
106             # Store the orig/init colspec data in 'init_colspecs'
107             has 'init_colspecs', is => 'ro', required => 1;
108             around BUILDARGS => sub {
109             my $orig = shift;
110             my $class = shift;
111             my %params = (ref($_[0]) eq 'HASH') ? %{ $_[0] } : @_; # <-- arg as hash or hashref
112             $params{init_colspecs} = [ @{$params{colspecs}} ] if (ref($params{colspecs}) eq 'ARRAY');
113             return $class->$orig(%params);
114             };
115              
116             sub BUILD {
117 1338     1338 0 984772 my $self = shift;
118 1338         3182 $self->regen_subspec;
119             }
120              
121             after 'expand_colspecs' => sub { (shift)->regen_subspec(@_) };
122             after 'add_colspecs' => sub { (shift)->regen_subspec(@_) };
123              
124              
125             sub expand_colspecs {
126             my $self = shift;
127             my $code = shift;
128            
129             @{$self->colspecs} = $code->(@{$self->colspecs});
130             }
131              
132              
133              
134             sub regen_subspec {
135 2426     2426 0 3422 my $self = shift;
136 2426         71035 $self->_clear_rel_order;
137 2426         62567 $self->_clear_subspec;
138 2426         66474 $self->_clear_subspec_data;
139 2426         52990 $self->subspec;
140             }
141              
142              
143             has 'rel_order', is => 'ro', lazy => 1, clearer => '_clear_rel_order', default => sub {
144             my $self = shift;
145             return $self->_subspec_data->{order};
146             }, isa => 'ArrayRef';
147 2602     2602 0 3400 sub all_rel_order { uniq( @{(shift)->rel_order} ) }
  2602         58203  
148 2426     2426 0 4152 sub count_rel_order { scalar( (shift)->all_rel_order ) }
149              
150             has 'subspec', is => 'ro', lazy => 1, clearer => '_clear_subspec', default => sub {
151             my $self = shift;
152             my $data = $self->_subspec_data->{data};
153             return { '' => $self } unless ($self->count_rel_order > 1);
154             return { map { $_ => __PACKAGE__->new(colspecs => $data->{$_}) } keys %$data };
155             }, isa => 'HashRef';
156 688     688 0 15119 sub get_subspec { (shift)->subspec->{$_[0]} }
157              
158              
159              
160             has '_subspec_data', is => 'ro', isa => 'HashRef', lazy => 1, clearer => '_clear_subspec_data',
161             default => sub {
162             my $self = shift;
163            
164             my @order = ('');
165             my %data = ('' => []);
166            
167             my %end_rels = ( '' => 1 );
168             foreach my $spec ($self->all_colspecs) {
169             my $pre; { my ($match) = ($spec =~ /^(\!)/); $spec =~ s/^(\!)//; $pre = $match ? $match : ''; }
170            
171             my @parts = split(/\./,$spec);
172             my $rel = shift @parts;
173             my $subspec = join('.',@parts);
174             unless(@parts > 0) { # <-- if its the base rel
175             $subspec = $rel;
176             $rel = '';
177             }
178            
179             # end rels that link to colspecs and not just to relspecs
180             # (intermediate rels with no direct columns)
181             $end_rels{$rel}++ if (
182             not $subspec =~ /\./ and
183             $pre eq ''
184             );
185            
186             unless(defined $data{$rel}) {
187             $data{$rel} = [];
188             push @order, $rel;
189             }
190            
191             push @{$data{$rel}}, $pre . $subspec;
192             }
193            
194             # Set the base colspec to '*' if its empty:
195             push @{$data{''}}, '*' unless (@{$data{''}} > 0);
196             $end_rels{$_} or push @{$data{$_}}, '!*' for (@order);
197            
198             return {
199             data => \%data,
200             order => \@order
201             };
202             };
203              
204             sub base_colspec {
205 352     352 0 640 my $self = shift;
206 352         892 return $self->get_subspec('');
207             }
208              
209              
210              
211              
212              
213             1;