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   37 use strict;
  5         12  
  5         168  
3 5     5   27 use warnings;
  5         9  
  5         157  
4              
5             # ABSTRACT: Data representation of schema for diffing
6              
7 5     5   28 use Moo;
  5         12  
  5         36  
8             with 'DBIx::Class::Schema::Diff::Role::Common';
9              
10 5     5   2113 use Types::Standard qw(:all);
  5         29  
  5         71  
11 5     5   245506 use Module::Runtime;
  5         15  
  5         68  
12 5     5   316 use Scalar::Util qw(blessed);
  5         12  
  5         394  
13 5     5   39 use Path::Class qw(file);
  5         12  
  5         285  
14 5     5   35 use JSON;
  5         12  
  5         50  
15 5     5   772 use Clone 'clone';
  5         14  
  5         296  
16 5     5   3267 use Digest::SHA1;
  5         4088  
  5         260  
17              
18 5     5   37 use Data::Dumper;
  5         15  
  5         318  
19 5     5   2511 use Data::Dumper::Concise;
  5         1839  
  5         8527  
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 345517 my $self = shift;
38              
39             # initialize:
40 52         1052 $self->data;
41             }
42              
43             sub sources {
44 106     106 0 1703 my $self = shift;
45 106 50       187 return sort keys %{ $self->data->{sources} || {} };
  106         1820  
46             }
47              
48             sub source {
49 1034     1034 0 2057 my ($self, $name) = @_;
50 1034         20273 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 3296 my ($self, $path) = @_;
60 1 50       5 die "Filename required" unless ($path);
61 1         7 my $file = file($path)->absolute;
62            
63 1 50       159 die "Target file '$file' already exists." if (-e $file);
64            
65 1         102 my $out_fh;
66 1 50       25 open $out_fh, '>', $file or die "Could not open $file: $!";
67 1         214 print $out_fh JSON::to_json( $self->data, { pretty => 1 });
68 1         1680 close $out_fh;
69 1 50       9 return -f $file ? 1 : 0;
70             }
71              
72              
73             sub _gen_data {
74 43     43   133 my ($self, $schema) = @_;
75            
76             my $data = {
77             schema_class => (blessed $schema),
78             sources => {
79             map {
80 43         378 my $Source = $schema->source($_);
  989         103919  
81             $_ => {
82            
83             columns => {
84             map {
85 5635         56280 $_ => $Source->column_info($_)
86             } $Source->columns
87             },
88            
89             relationships => {
90             map {
91 1849         16636 $_ => $Source->relationship_info($_)
92             } $Source->relationships
93             },
94            
95             constraints => {
96             map {
97 989         37666 $_ => { columns => [$Source->unique_constraint_columns($_)] }
  785         14429  
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         5176 return $self->_localize_deep_namespace_strings($data,$data->{schema_class});
111             }
112              
113              
114             sub _coerce_schema {
115 52     52   25619 my ($v) = @_;
116 52 100 100     1047 return $v if (!$v || ref $v);
117            
118             # Its a class name:
119 14         87 Module::Runtime::require_module($v);
120 14 50       706452 return $v->can('connect') ? $v->connect('dbi:SQLite::memory:','','') : $v;
121             }
122              
123              
124             sub _coerce_deep_unsafe_refs {
125 105945     105945   282525 my ($v) = @_;
126 105945 100       298193 my $rt = ref($v) or return $v;
127            
128 28796 100       54245 if($rt eq 'HASH') {
    100          
    100          
129 23854         65828 return { map { $_ => &_coerce_deep_unsafe_refs($v->{$_}) } keys %$v };
  74892         136464  
130             }
131             elsif($rt eq 'ARRAY') {
132 2663         5319 return [ map { &_coerce_deep_unsafe_refs($_) } @$v ];
  31001         50281  
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         5377 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         1963 my $str = Dumper($v);
144             # strip newlines:
145 645         68760 $str =~ s/\r?\n//g;
146             # normalize whitespace:
147 645         1691 $str =~ s/\s+/ /g;
148 645         2613 return $str;
149             }
150             }
151              
152             sub _localize_deep_namespace_strings {
153 88276     88276   164183 my ($self, $v, $ns) = @_;
154 88276         131701 my $rt = ref($v);
155 88276 100       145144 if($rt) {
156 24308 100       42171 if($rt eq 'HASH') {
    100          
157             return { map {
158 19793         58641 $_ => $self->_localize_deep_namespace_strings($v->{$_},$ns)
  62121         125953  
159             } keys %$v };
160             }
161             elsif($rt eq 'ARRAY') {
162             return [ map {
163 2236         4791 $self->_localize_deep_namespace_strings($_,$ns)
  26112         47948  
164             } @$v ];
165             }
166             else {
167 2279         8127 return $v;
168             }
169             }
170             else {
171             # swap the namespace prefix string for literal '{schema_class}':
172 63968 100 66     284345 $v =~ s/^${ns}/\{schema_class\}/ if($v && $ns && $v ne $ns);
      100        
173 63968         218299 return $v;
174             }
175             }
176              
177              
178             sub prune {
179 2     2 1 11 my ($self,@keywords) = @_;
180 2 50       12 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         4233 my $meth = join('_','__prune',$_);
  3         13  
186 3 50       24 $self->can($meth) or die "Bad prune keyword '$_' (no such method '$meth')";
187 3         16 $meth
188             } @keywords;
189            
190 2         13 $self->$_($data) for (@meths);
191              
192 2         66 __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   4 my ($self, $data) = @_;
203 1         5 $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       49 if(my $columns = $rsrcData->{columns}) {
222 23         53 for my $attrs (values %$columns) {
223             # delete all keys starting with underscore '_'
224 131   66     862 $_ =~ /^_/ and delete $attrs->{$_} for (keys %$attrs);
225             }
226             }
227             }
228            
229             $data
230 1         6 }
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         11  
237 46 50       152 delete $rsrcData->{$key} if exists $rsrcData->{$key}
238             }
239            
240             $data
241 2         9 }
242              
243              
244             sub fingerprint {
245 3     3 1 170763 my $self = shift;
246 3         38 my $sum = Digest::SHA1->new->add( $self->_string_for_signature )->hexdigest;
247 3         21719 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         8 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__