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   42 use strict;
  5         9  
  5         177  
3 5     5   27 use warnings;
  5         11  
  5         151  
4              
5             # ABSTRACT: Data representation of schema for diffing
6              
7 5     5   29 use Moo;
  5         12  
  5         37  
8             with 'DBIx::Class::Schema::Diff::Role::Common';
9              
10 5     5   2123 use Types::Standard qw(:all);
  5         22  
  5         62  
11 5     5   247461 use Module::Runtime;
  5         17  
  5         76  
12 5     5   392 use Scalar::Util qw(blessed);
  5         13  
  5         429  
13 5     5   43 use Path::Class qw(file);
  5         15  
  5         262  
14 5     5   42 use JSON;
  5         12  
  5         54  
15 5     5   871 use Clone 'clone';
  5         15  
  5         303  
16 5     5   3499 use Digest::SHA1;
  5         4138  
  5         258  
17              
18 5     5   40 use Data::Dumper;
  5         11  
  5         305  
19 5     5   2480 use Data::Dumper::Concise;
  5         1794  
  5         8570  
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 337245 my $self = shift;
38              
39             # initialize:
40 52         1128 $self->data;
41             }
42              
43             sub sources {
44 106     106 0 1622 my $self = shift;
45 106 50       199 return sort keys %{ $self->data->{sources} || {} };
  106         1814  
46             }
47              
48             sub source {
49 1034     1034 0 2084 my ($self, $name) = @_;
50 1034         19248 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 985 my ($self, $path) = @_;
60 1 50       6 die "Filename required" unless ($path);
61 1         8 my $file = file($path)->absolute;
62            
63 1 50       143 die "Target file '$file' already exists." if (-e $file);
64            
65 1         91 my $out_fh;
66 1 50       19 open $out_fh, '>', $file or die "Could not open $file: $!";
67 1         209 print $out_fh JSON::to_json( $self->data, { pretty => 1 });
68 1         1390 close $out_fh;
69 1 50       10 return -f $file ? 1 : 0;
70             }
71              
72              
73             sub _gen_data {
74 43     43   166 my ($self, $schema) = @_;
75            
76             my $data = {
77             schema_class => (blessed $schema),
78             sources => {
79             map {
80 43         376 my $Source = $schema->source($_);
  989         101447  
81             $_ => {
82            
83             columns => {
84             map {
85 5635         54826 $_ => $Source->column_info($_)
86             } $Source->columns
87             },
88            
89             relationships => {
90             map {
91 1849         16495 $_ => $Source->relationship_info($_)
92             } $Source->relationships
93             },
94            
95             constraints => {
96             map {
97 989         36147 $_ => { columns => [$Source->unique_constraint_columns($_)] }
  785         13537  
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         5346 return $self->_localize_deep_namespace_strings($data,$data->{schema_class});
111             }
112              
113              
114             sub _coerce_schema {
115 52     52   24476 my ($v) = @_;
116 52 100 100     1025 return $v if (!$v || ref $v);
117            
118             # Its a class name:
119 14         94 Module::Runtime::require_module($v);
120 14 50       700563 return $v->can('connect') ? $v->connect('dbi:SQLite::memory:','','') : $v;
121             }
122              
123              
124             sub _coerce_deep_unsafe_refs {
125 105945     105945   266044 my ($v) = @_;
126 105945 100       290827 my $rt = ref($v) or return $v;
127            
128 28796 100       53588 if($rt eq 'HASH') {
    100          
    100          
129 23854         65417 return { map { $_ => &_coerce_deep_unsafe_refs($v->{$_}) } keys %$v };
  74892         133087  
130             }
131             elsif($rt eq 'ARRAY') {
132 2663         4890 return [ map { &_coerce_deep_unsafe_refs($_) } @$v ];
  31001         47568  
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         5334 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         2146 my $str = Dumper($v);
144             # strip newlines:
145 645         66948 $str =~ s/\r?\n//g;
146             # normalize whitespace:
147 645         1768 $str =~ s/\s+/ /g;
148 645         2355 return $str;
149             }
150             }
151              
152             sub _localize_deep_namespace_strings {
153 88276     88276   158240 my ($self, $v, $ns) = @_;
154 88276         129917 my $rt = ref($v);
155 88276 100       141801 if($rt) {
156 24308 100       41104 if($rt eq 'HASH') {
    100          
157             return { map {
158 19793         56698 $_ => $self->_localize_deep_namespace_strings($v->{$_},$ns)
  62121         124637  
159             } keys %$v };
160             }
161             elsif($rt eq 'ARRAY') {
162             return [ map {
163 2236         4324 $self->_localize_deep_namespace_strings($_,$ns)
  26112         46408  
164             } @$v ];
165             }
166             else {
167 2279         7300 return $v;
168             }
169             }
170             else {
171             # swap the namespace prefix string for literal '{schema_class}':
172 63968 100 66     282678 $v =~ s/^${ns}/\{schema_class\}/ if($v && $ns && $v ne $ns);
      100        
173 63968         213882 return $v;
174             }
175             }
176              
177              
178             sub prune {
179 2     2 1 8 my ($self,@keywords) = @_;
180 2 50       11 die "must supply at least 1 prune keyword" unless (scalar(@keywords) > 0);
181            
182 2         54 my $data = clone( $self->data );
183            
184             my @meths = map {
185 2         4188 my $meth = join('_','__prune',$_);
  3         14  
186 3 50       21 $self->can($meth) or die "Bad prune keyword '$_' (no such method '$meth')";
187 3         15 $meth
188             } @keywords;
189            
190 2         15 $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         5 $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         6  
221 23 50       54 if(my $columns = $rsrcData->{columns}) {
222 23         55 for my $attrs (values %$columns) {
223             # delete all keys starting with underscore '_'
224 131   66     678 $_ =~ /^_/ and delete $attrs->{$_} for (keys %$attrs);
225             }
226             }
227             }
228            
229             $data
230 1         4 }
231              
232              
233             sub _prune_whole_source_key {
234 2     2   6 my ($self, $key, $data) = @_;
235            
236 2         4 for my $rsrcData (values %{ $data->{sources} }) {
  2         10  
237 46 50       144 delete $rsrcData->{$key} if exists $rsrcData->{$key}
238             }
239            
240             $data
241 2         8 }
242              
243              
244             sub fingerprint {
245 3     3 1 168776 my $self = shift;
246 3         39 my $sum = Digest::SHA1->new->add( $self->_string_for_signature )->hexdigest;
247 3         21441 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   8 my $self = shift;
255            
256 3         10 local $Data::Dumper::Maxdepth = 0;
257 3         118 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__