File Coverage

blib/lib/DBIx/Class/Schema/Diff/SchemaData.pm
Criterion Covered Total %
statement 118 124 95.1
branch 28 38 73.6
condition 10 12 83.3
subroutine 27 30 90.0
pod 4 7 57.1
total 187 211 88.6


line stmt bran cond sub pod time code
1             package DBIx::Class::Schema::Diff::SchemaData;
2 5     5   40 use strict;
  5         11  
  5         174  
3 5     5   30 use warnings;
  5         10  
  5         152  
4              
5             # ABSTRACT: Data representation of schema for diffing
6              
7 5     5   26 use Moo;
  5         16  
  5         47  
8             with 'DBIx::Class::Schema::Diff::Role::Common';
9              
10 5     5   2033 use Types::Standard qw(:all);
  5         22  
  5         55  
11 5     5   243227 use Module::Runtime;
  5         18  
  5         54  
12 5     5   316 use Scalar::Util qw(blessed);
  5         13  
  5         415  
13 5     5   42 use Path::Class qw(file);
  5         24  
  5         250  
14 5     5   33 use JSON;
  5         13  
  5         58  
15 5     5   788 use Clone 'clone';
  5         14  
  5         292  
16 5     5   3212 use Digest::SHA1;
  5         3909  
  5         231  
17              
18 5     5   3232 use Data::Dumper;
  5         35181  
  5         341  
19 5     5   2289 use Data::Dumper::Concise;
  5         1723  
  5         8049  
20              
21             has 'schema', is => 'ro', isa => Maybe[InstanceOf[
22             'DBIx::Class::Schema'
23             ]], coerce => \&_coerce_schema, default => sub {undef};
24              
25             has 'data', is => 'ro', lazy => 1, default => sub {
26             my $self = shift;
27            
28             die "You must supply a schema or an existing data packet"
29             unless ($self->schema);
30            
31             return $self->_gen_data( $self->schema );
32              
33             }, isa => HashRef, coerce => \&_coerce_deep_unsafe_refs;
34              
35              
36             sub BUILD {
37 52     52 0 330622 my $self = shift;
38              
39             # initialize:
40 52         1063 $self->data;
41             }
42              
43             sub sources {
44 106     106 0 1613 my $self = shift;
45 106 50       208 return sort keys %{ $self->data->{sources} || {} };
  106         1995  
46             }
47              
48             sub source {
49 1034     1034 0 2053 my ($self, $name) = @_;
50 1034         19348 return $self->data->{sources}{$name};
51             }
52              
53             sub dump_json {
54 0     0 1 0 my $self = shift;
55 0         0 JSON::to_json( $self->data => { pretty => 1 });
56             }
57              
58             sub dump_json_file {
59 1     1 1 896 my ($self, $path) = @_;
60 1 50       5 die "Filename required" unless ($path);
61 1         7 my $file = file($path)->absolute;
62            
63 1 50       144 die "Target file '$file' already exists." if (-e $file);
64            
65 1         86 my $out_fh;
66 1 50       19 open $out_fh, '>', $file or die "Could not open $file: $!";
67 1         248 print $out_fh JSON::to_json( $self->data, { pretty => 1 });
68 1         1380 close $out_fh;
69 1 50       11 return -f $file ? 1 : 0;
70             }
71              
72              
73             sub _gen_data {
74 43     43   136 my ($self, $schema) = @_;
75            
76             my $data = {
77             schema_class => (blessed $schema),
78             sources => {
79             map {
80 43         403 my $Source = $schema->source($_);
  989         99760  
81             $_ => {
82            
83             columns => {
84             map {
85 5635         53340 $_ => $Source->column_info($_)
86             } $Source->columns
87             },
88            
89             relationships => {
90             map {
91 1849         16092 $_ => $Source->relationship_info($_)
92             } $Source->relationships
93             },
94            
95             constraints => {
96             map {
97 989         36057 $_ => { columns => [$Source->unique_constraint_columns($_)] }
  785         13332  
98             } $Source->unique_constraint_names
99             },
100            
101             table_name => $Source->from,
102            
103             isa => mro::get_linear_isa( $schema->class( $Source->source_name ) ),
104              
105             }
106             } $schema->sources
107             }
108             };
109            
110 43         4936 return $self->_localize_deep_namespace_strings($data,$data->{schema_class});
111             }
112              
113              
114             sub _coerce_schema {
115 52     52   24501 my ($v) = @_;
116 52 100 100     1047 return $v if (!$v || ref $v);
117            
118             # Its a class name:
119 14         78 Module::Runtime::require_module($v);
120 14 50       689725 return $v->can('connect') ? $v->connect('dbi:SQLite::memory:','','') : $v;
121             }
122              
123              
124             sub _coerce_deep_unsafe_refs {
125 105945     105945   266535 my ($v) = @_;
126 105945 100       291485 my $rt = ref($v) or return $v;
127            
128 28796 100       53218 if($rt eq 'HASH') {
    100          
    100          
129 23854         64002 return { map { $_ => &_coerce_deep_unsafe_refs($v->{$_}) } keys %$v };
  74892         133930  
130             }
131             elsif($rt eq 'ARRAY') {
132 2663         4710 return [ map { &_coerce_deep_unsafe_refs($_) } @$v ];
  31001         47540  
133             }
134             elsif($rt eq 'CODE') {
135             # TODO: we don't have to do this, we could let it through
136             # to be stringified, but for now, we're not trying to compare
137             # CodeRef contents
138 1634         5441 return 'sub { "DUMMY" }';
139             }
140             else {
141             # For all other refs, stringify them with Dumper. These will still
142             # be just as useful for diff/compare. This makes them safe for JSON, etc
143 645         2329 my $str = Dumper($v);
144             # strip newlines:
145 645         65805 $str =~ s/\r?\n//g;
146             # normalize whitespace:
147 645         1679 $str =~ s/\s+/ /g;
148 645         2286 return $str;
149             }
150             }
151              
152             sub _localize_deep_namespace_strings {
153 88276     88276   157551 my ($self, $v, $ns) = @_;
154 88276         129150 my $rt = ref($v);
155 88276 100       141198 if($rt) {
156 24308 100       41171 if($rt eq 'HASH') {
    100          
157             return { map {
158 19793         55181 $_ => $self->_localize_deep_namespace_strings($v->{$_},$ns)
  62121         124104  
159             } keys %$v };
160             }
161             elsif($rt eq 'ARRAY') {
162             return [ map {
163 2236         4327 $self->_localize_deep_namespace_strings($_,$ns)
  26112         48043  
164             } @$v ];
165             }
166             else {
167 2279         7219 return $v;
168             }
169             }
170             else {
171             # swap the namespace prefix string for literal '{schema_class}':
172 63968 100 66     276914 $v =~ s/^${ns}/\{schema_class\}/ if($v && $ns && $v ne $ns);
      100        
173 63968         212702 return $v;
174             }
175             }
176              
177              
178             sub prune {
179 2     2 1 10 my ($self,@keywords) = @_;
180 2 50       11 die "must supply at least 1 prune keyword" unless (scalar(@keywords) > 0);
181            
182 2         56 my $data = clone( $self->data );
183            
184             my @meths = map {
185 2         4442 my $meth = join('_','__prune',$_);
  3         13  
186 3 50       23 $self->can($meth) or die "Bad prune keyword '$_' (no such method '$meth')";
187 3         15 $meth
188             } @keywords;
189            
190 2         11 $self->$_($data) for (@meths);
191              
192 2         70 __PACKAGE__->new({ data => $data })
193             }
194              
195              
196             sub __prune_isa {
197 1     1   5 my ($self, $data) = @_;
198 1         6 $self->_prune_whole_source_key('isa',$data)
199             }
200              
201             sub __prune_constraints {
202 1     1   5 my ($self, $data) = @_;
203 1         3 $self->_prune_whole_source_key('constraints',$data)
204             }
205              
206             sub __prune_relationships {
207 0     0   0 my ($self, $data) = @_;
208 0         0 $self->_prune_whole_source_key('relationships',$data)
209             }
210              
211             sub __prune_columns {
212 0     0   0 my ($self, $data) = @_;
213 0         0 $self->_prune_whole_source_key('columns',$data)
214             }
215              
216              
217             sub __prune_private_col_attrs {
218 1     1   4 my ($self, $data) = @_;
219            
220 1         3 for my $rsrcData (values %{ $data->{sources} }) {
  1         9  
221 23 50       50 if(my $columns = $rsrcData->{columns}) {
222 23         52 for my $attrs (values %$columns) {
223             # delete all keys starting with underscore '_'
224 131   66     647 $_ =~ /^_/ and delete $attrs->{$_} for (keys %$attrs);
225             }
226             }
227             }
228            
229             $data
230 1         5 }
231              
232              
233             sub _prune_whole_source_key {
234 2     2   5 my ($self, $key, $data) = @_;
235            
236 2         6 for my $rsrcData (values %{ $data->{sources} }) {
  2         10  
237 46 50       145 delete $rsrcData->{$key} if exists $rsrcData->{$key}
238             }
239            
240             $data
241 2         8 }
242              
243              
244             sub fingerprint {
245 3     3 1 167785 my $self = shift;
246 3         42 my $sum = Digest::SHA1->new->add( $self->_string_for_signature )->hexdigest;
247 3         21389 join('-', 'schemsum', substr($sum,0,15) )
248             }
249              
250              
251             # So far this is the only thing I could find to produce a consistent string value across all
252             # Travis tested perls (5.10,5.12,5.14,5.16,5.18,5.20,5.22,5.24,5.26)
253             sub _string_for_signature {
254 3     3   9 my $self = shift;
255            
256 3         10 local $Data::Dumper::Maxdepth = 0;
257 3         125 Data::Dumper->new([ $self->data->{sources} ])
258             ->Purity(0)
259             ->Terse(1)
260             ->Indent(0)
261             ->Useqq(1)
262             ->Sortkeys(1)
263             ->Dump()
264             }
265              
266              
267             1;
268              
269              
270             __END__