File Coverage

blib/lib/Bigtop/ScriptHelp/Style/Kickstart.pm
Criterion Covered Total %
statement 105 132 79.5
branch 47 58 81.0
condition 13 22 59.0
subroutine 12 15 80.0
pod 8 8 100.0
total 185 235 78.7


line stmt bran cond sub pod time code
1             package Bigtop::ScriptHelp::Style::Kickstart;
2 1     1   18 use strict; use warnings;
  1     1   3  
  1         32  
  1         5  
  1         2  
  1         27  
3              
4 1     1   4 use base 'Bigtop::ScriptHelp::Style';
  1         1  
  1         88  
5              
6 1     1   1016 use Text::Balanced qw( extract_multiple extract_bracketed );
  1         27246  
  1         145  
7              
8 1     1   922 use Bigtop::ScriptHelp qw( valid_ident );
  1         5  
  1         201  
9              
10             sub get_default_base_columns {
11             return {
12 21     21 1 340 is_normal_default => {
13             id => {
14             where => 'front',
15             types => [ 'int4', 'primary_key', 'auto' ]
16             },
17             created => {
18             where => 'rear',
19             types => [ 'datetime' ],
20             },
21             modified => {
22             where => 'rear',
23             types => [ 'datetime' ],
24             },
25             },
26             normal_defaults => [
27             qw( id created modified )
28             ],
29             };
30             }
31              
32             sub get_default_filler_columns {
33 6     6 1 21 return 'ident,description';
34             }
35              
36             sub get_db_layout {
37 6     6 1 96443 my $self = shift;
38 6   50     56 my $art = shift || '';
39 6   100     37 my $tables = shift || {};
40              
41             {
42 1     1   53 no warnings; # don't tell me about unsuccessful stats on $art
  1         4  
  1         1740  
  6         8  
43              
44 6 100       160 if ( -f $art ) { # take art from file
45              
46 1         57 open my $ART, '<', $art;
47 1         44 my $actual_art = join '', <$ART>;
48 1         31 close $ART;
49              
50 1         6 $art = $actual_art;
51             }
52             }
53              
54 6         11 my @new_tables;
55             my @joiners;
56 0         0 my %foreign_key_for;
57 0         0 my %columns;
58 6         20 my $default_columns = $self->get_default_filler_columns();
59              
60 6         32 $art =~ s/^\s+//;
61 6         37 $art =~ s/\s+$//;
62              
63 6         32 foreach my $art_element ( split /\s+/, $art ) {
64 27 100       157 if ( $art_element =~ /<|-|>/ ) {
    100          
65             # split tables from operator
66 18         106 my ( $table1, $op, $table2 ) =
67             split /(<->|->|<-|\*>|<\*|-)/, $art_element;
68              
69             # now pull column descriptions, if present
70 18         23 my ( $cols1, $cols2 );
71              
72 18         46 ( $table1, $cols1 ) = $self->get_columns( $table1 );
73 18         50 ( $table2, $cols2 ) = $self->get_columns( $table2 );
74              
75 18 50 33     88 unless ( defined $table1 and valid_ident( $table1 )
      33        
      33        
      33        
76             and
77             defined $table2 and valid_ident( $table2 )
78             and
79             defined $op
80             ) {
81 0         0 die "Invalid ASCII art (1): $art_element\n";
82             }
83              
84             # make sure tables are in the list of all tables
85 18 100       48 unless ( defined $tables->{ $table1 } ) {
86 9         17 push @new_tables, $table1;
87 9         28 $tables->{ $table1 }++;
88             }
89 18 100       39 $columns{ $table1 } = $cols1 if defined $cols1;
90              
91 18 100       42 unless ( defined $tables->{ $table2 } ) {
92 10         16 push @new_tables, $table2;
93 10         23 $tables->{ $table2 }++;
94             }
95 18 100       33 $columns{ $table2 } = $cols2 if defined $cols2;
96              
97             # process based on operator
98 18 100 100     121 if ( $op eq '<-' or $op eq '*>' ) {
    100 100        
    50          
    50          
99 5         8 push @{ $foreign_key_for{ $table2 } },
  5         32  
100             { table => $table1, col => 1};
101             }
102             elsif ( $op eq '->' or $op eq '<*' ) {
103 7         8 push @{ $foreign_key_for{ $table1 } },
  7         159  
104             { table => $table2, col => 1};
105             }
106             elsif ( $op eq '-' ) {
107 0         0 push @{ $foreign_key_for{ $table2 } },
  0         0  
108             { table => $table1, col => 1 };
109 0         0 push @{ $foreign_key_for{ $table1 } },
  0         0  
110             { table => $table2, col => 1 };
111             }
112             elsif ( $op eq '<->' ) {
113 6         22 push @joiners, [ $table1, $table2 ];
114             }
115             else {
116 0         0 die "Invalid ASCII art (2): $art_element\n";
117             }
118             }
119             elsif ( valid_ident( $art_element ) ) {
120 1 50       4 unless ( defined $tables->{ $art_element } ) {
121 1         2 push @new_tables, $art_element;
122 1         2 $tables->{ $art_element }++;
123 1 50       5 $columns{ $art_element } =
124             $self->parse_columns( $default_columns )
125             unless $columns{ $art_element };
126             }
127             }
128             else {
129 8         26 my ( $table, $cols ) = $self->get_columns( $art_element );
130              
131 8 50       29 unless ( valid_ident( $table ) ) {
132 0         0 die "Invalid ASCII art (3): $art_element\n";
133             }
134              
135 8 100       30 unless ( defined $tables->{ $table } ) {
136 2         5 push @new_tables, $table;
137 2         3 $tables->{ $table }++;
138             }
139 8 50       34 $columns{ $table } = $cols if defined $cols;
140             }
141             }
142              
143 6         20 my $parsed_defaults = $self->parse_columns( $default_columns );
144             NEW_TABLE:
145 6         17 foreach my $new_table ( @new_tables ) { # add default cols as needed
146 22 100       71 next NEW_TABLE if defined $columns{ $new_table };
147 7         16 $columns{ $new_table } = $parsed_defaults;
148             }
149              
150             return {
151 6         92 all_tables => $tables,
152             new_tables => \@new_tables,
153             joiners => \@joiners,
154             foreigners => \%foreign_key_for,
155             columns => \%columns,
156             }
157             }
158              
159             sub get_columns {
160 44     44 1 62 my $self = shift;
161 44         54 my $table = shift;
162              
163             my ( $name, $raw ) = extract_multiple(
164             $table, [
165 14     14   1061 qr/([^(]*)/, sub { extract_bracketed( $_[0], '()' ) }
166 44         326 ]
167             );
168              
169 44 100       5210 if ( defined $raw ) {
170              
171 14         58 $raw =~ s/^\(//;
172 14         49 $raw =~ s/\)$//;
173              
174 14         43 return ( $name, $self->parse_columns( $raw ) );
175             }
176             else {
177 30         95 return ( $name );
178             }
179             }
180              
181             sub parse_columns {
182 21     21 1 29 my $self = shift;
183 21         29 my $raw = shift;
184              
185 21         75 my @pieces = split /,/, $raw;
186              
187 21         36 my $you_dont_want_em = 0;
188 21         51 my $defaults = $self->get_default_base_columns;
189 21         42 my $is_normal_default = $defaults->{ is_normal_default };
190 21         24 my %you_dont_want;
191              
192             my @columns;
193 21         43 foreach my $piece ( @pieces ) {
194 73         254 my ( $name_type, $default ) = split /=/, $piece;
195 73         161 my ( $name, @types ) = split /:/, $name_type;
196              
197             # pull optional plus from name
198 73         80 my $optional;
199 73 100       235 $optional = 1 if ( $name =~ s/^\+// );
200              
201 73 100       260 @types = ( 'varchar' ) unless ( @types > 0 );
202              
203             # begin building columns sub hash with required keys
204 73         305 my %col_hash = ( name => $name, types => \@types );
205              
206             # fill in other keys if we need them
207 73 100       226 $col_hash{ default } = $default if $default;
208 73 100       215 $col_hash{ optional } = $optional if $optional;
209              
210 73         107 push @columns, \%col_hash;
211              
212 73 100       260 $you_dont_want{ $name }++ if defined $is_normal_default->{ $name };
213             }
214              
215             COL:
216 21         32 foreach my $col ( @{ $defaults->{ normal_defaults } } ) {
  21         45  
217 63 100       406 unless ( $you_dont_want{ $col } ) {
218 60 50       146 if ( not defined $is_normal_default->{ $col }{ where } ) {
219 0         0 warn "normal_defaults column '$col' has no where "
220             . "in is_normal_default\n";
221 0         0 next COL;
222             }
223 60 50       134 if ( not defined $is_normal_default->{ $col }{ types } ) {
224 0         0 warn "normal_defaults column '$col' has no types "
225             . "in is_normal_default\n";
226 0         0 next COL;
227             }
228 60 100       127 if ( $is_normal_default->{ $col }{ where } eq 'front' ) {
229             unshift @columns, {
230             name => $col,
231             types => $is_normal_default->{ $col }{ types },
232             }
233 19         97 }
234             else {
235             push @columns, {
236             name => $col,
237             types => $is_normal_default->{ $col }{ types },
238             }
239 41         8131 }
240             }
241             }
242              
243 21         205 return \@columns;
244             }
245              
246             sub print_instructions {
247 0     0 1   my $class = shift;
248 0           my $app_name = shift;
249 0           my $build_dir = shift;
250 0           my $built_sqlite = shift;
251              
252 0           my $heading = << "EO_SQLite_Basic";
253              
254             I have generated your '$app_name' application. To run the application:
255              
256             cd $build_dir
257             sqlite app.db < docs/schema.sqlite
258             ./app.server [ port ]
259             EO_SQLite_Basic
260              
261 0 0         if ( $built_sqlite ) {
262 0           $heading = << "EO_SQLite_Prebuilt";
263              
264             I have generated your '$app_name' application. I have also taken the liberty
265             of making an sqlite database for it to use. To run the application:
266              
267             cd $build_dir
268             ./app.server [ port ]
269             EO_SQLite_Prebuilt
270             }
271              
272 0           print << "EO_Instructions";
273             $heading
274             The app.server runs on port 8080 by default.
275              
276             Once the app.server starts, it will print a list of the urls it can serve.
277             Point your browser to one of those and enjoy.
278              
279             If you prefer to run the app with Postgres or MySQL type one of these:
280              
281             bigtop --pg_help
282             bigtop --mysql_help
283              
284             EO_Instructions
285              
286             }
287              
288             sub help_pg {
289 0     0 1   print << "EO_pg_help";
290              
291             For PostgreSQL, in your build directory:
292              
293             createdb app.db -U postgres
294             psql app.db -U regular_user < docs/schema.postgres
295             ./app.server --dbd=Pg --dbuser=regular_user --dppass='secret'
296              
297             Supply passwords as prompted when creating and building the database.
298             You may abbreviate --dbd as -d, --dbuser as -u, and --dbpass as -p.
299              
300             If you change the name from app.db, supply --dbname (aka -n) to app.server.
301              
302             EO_pg_help
303              
304 0           exit 0;
305             }
306              
307             sub help_mysql {
308 0     0 1   my $class = shift;
309 0           my $build_dir = shift;
310              
311 0           print << "EO_mysql_help";
312              
313             For MySQL, in your build directory:
314              
315             mysqladmin create app_db -u root -p
316             mysql -u root -p app_db < docs/schema.mysql
317             ./app.server --dbd=mysql --dbuser=regular_user \
318             --dbpass='secret' --dbname=app_db
319              
320             Supply passwords as prompted when creating and building the database.
321             You may abbreviate --dbd as -d, --dbuser as -u, --dbpass as -p,
322             and --dbname as -n.
323              
324             EO_mysql_help
325              
326 0           exit 0;
327             }
328              
329             1;
330              
331             =head1 NAME
332              
333             Bigtop::ScriptHelp::Style::Kickstart - handles kickstart syntax for scripts
334              
335             =head1 SYNOPSIS
336              
337             Most users use this module as the default style for the bigtop and
338             tentmaker scripts:
339              
340             bigtop -n AppName [kickstart]
341              
342             See L below for details, but note that kickstart could
343             be a file whose contents are in kickstart syntax.
344              
345             If you are writing a script that wants to leverage styles do this:
346              
347             use Bigtop::ScriptHelp::Style;
348              
349             my $style = Bigtop::ScriptHelp::Style->get_style( 'Kickstart' );
350              
351             # then pass $style to methods of Bigtop::ScriptHelp
352              
353             =head1 DESCRIPTION
354              
355             See C for a description of what this module
356             must do in general.
357              
358             =head1 SUBCLASSING
359              
360             As of version 0.33, this module has been revised to make subclassing
361             easier. This allows you complete control over what columns are generated
362             by default. All you need to do is subclass this module and implement
363             two methods: C and C.
364             But, you can rely on this module to handle the original kickstart syntax.
365              
366             =head1 METHODS
367              
368             =over 4
369              
370             =item get_columns
371              
372             This method is for internal use, but is exposed so subclasses don't have
373             to implement it. It specifies that definitions like
374              
375             tablename(col,col2:int)
376              
377             will be parsed with C (see below).
378              
379             =item get_db_layout
380              
381             This method does not use standard in. Instead, it expects
382             kickstart syntax. See L below.
383              
384             =item get_default_base_columns
385              
386             Subclasses override this method to control which columns are created by
387             default. Note that if a user explicitly declares a column in the
388             parentheses of a table definition, the corresponding column in this
389             collection is ignored.
390              
391             Returns a hash with two keys: C and C.
392             C is a hash keyed by the name of each column with two
393             keys: C (either front or rear) and C (an anonymous array
394             of Bigtop C values, usually these are SQL types or other column
395             definition phrases). C is an anonymous array of column
396             names, they must correspond to keys in C. The order
397             of the columns in the array controls when they are added to the list of
398             columns. But, remember that C decides whether to unshift or push them
399             into the list.
400              
401             For example, the method in this module does this:
402              
403             return {
404             is_normal_default => {
405             id => {
406             where => 'front',
407             types => [ 'int4', 'primary_key', 'auto' ]
408             },
409             created => {
410             where => 'rear',
411             types => [ 'datetime' ],
412             },
413             modified => {
414             where => 'rear',
415             types => [ 'datetime' ],
416             },
417             },
418             normal_defaults => [
419             qw( id created modified )
420             ],
421             };
422              
423              
424             =item get_default_filler_columns
425              
426             Suppose a table is in the kickstart file, but has no column definitions.
427             It will get columns. First, this method returns a string which is
428             the default art for the table. Second, as with all tables, columns
429             returned by C are added.
430              
431             The method in this module returns:
432              
433             return 'ident,description';
434              
435             If you return the empty string, no filler columns will be added to
436             your tables.
437              
438             =item parse_columns
439              
440             This method is exposed so subclasses don't have to implement it. It
441             receives a kickstart art string and parses it into an anonymous array
442             of columns. Each element in the array is a hash with keys C (the
443             name of the SQL column) and C (an anonymous hash of Bigtop
444             C values, which are an SQL type and a list of other column definition
445             phrases). There are other keys: C and C. These
446             are used when the art includes C<=> or C<+>.
447              
448             =item print_instructions
449              
450             Called by bigtop script when it makes a new application containing
451             table definitions (you used art with this style). Prints instructions
452             on how to start the development application using sqlite.
453              
454             =item help_pg
455              
456             Revises the instructions from C so they are good
457             for PostgreSQL.
458              
459             =item help_mysql
460              
461             Revises the instructions from C so they are good
462             for mysql.
463              
464             =back
465              
466             =head1 KICKSTART SYNTAX
467              
468             Bigtop's kickstart syntax allows you to describe your tables, their columns,
469             and how they are related to other tables in a compressed text style.
470              
471             Note well: Since the descriptions use punctuation that your shell probably
472             loves, you must surround them with single quotes on the command line. But,
473             there's no need to do that if you put the kickstart description in a file.
474             To use the file method, put your kickstart in a file and give that
475             file's name as in:
476              
477             tentmaker -a docs/app.bigtop kickstart_file
478              
479             It is easiest to understand kickstart syntax is by seeing an example. So,
480             suppose we have a four table data model describing a bit of our personnel
481             process:
482              
483             +-----------+ +----------+
484             | job |<------| position |
485             +-----------+ +----------+
486             ^
487             |
488             +-----------+ +----------+
489             | job_skill |------>| skill |
490             +-----------+ +----------+
491              
492             What this data model shows is that each position refers to a job,
493             each job could require many skills, and each skill could be associated with
494             many jobs. The last two mean that job and skill share a many-to-many
495             relationship.
496              
497             Here's how to specify this data model with bigtop kickstart syntax:
498              
499             bigtop --new HR 'job<-position job<->skill'
500              
501             This indicates a foreign key from position to job and an implied
502             table, called job_skill, to hold the many-to-many relationship between
503             job and skill.
504              
505             The same kickstart can be used with --new and --add for both bigtop
506             and tentmaker scripts.
507              
508             There are four kickstart table relationship operators:
509              
510             =over 4
511              
512             =item <->
513              
514             Many-to-many. A new table will be made with foreign keys to each operand
515             table. Each operand table will have a has_many relationship. Note
516             that your Model backend may not understand these relationships. At the
517             time of this writing only Model GantryDBIxClass did, by luck it happens
518             to be the default.
519              
520             =item <- or *>
521              
522             The second table has a foreign key pointing to the first.
523              
524             The *> form is useful if you want to read the relationship with the phrase
525             'has-many' as in
526              
527             book*>chapter
528              
529             Each book has many chapters. Instead of
530              
531             book<-chapter
532              
533             Each chapter belongs to a book. But, both forms are equivalent.
534              
535             =item -> or <*
536              
537             The first table has a foreign key pointing to the second. This is really
538             a convenience synonymn for <-.
539              
540             Note that tables will appear in the generated SQL so that foreign keys
541             appear after the tables they refer to (at least that is the goal). Hence
542             the order of your tables in the kickstart has no bearing on
543             their order in the bigtop file.
544              
545             =item -
546              
547             The two tables have a one-to-one relationship. Each of them will have
548             a foreign key pointing to the other. Note that this will create SQL which
549             is unlikely to load well due to foreign key forward references.
550              
551             =back
552              
553             =head2 COLUMN DEFINITIONS
554              
555             As of Bigtop 0.23, you may use the syntax below to specify information
556             about the columns in your tables, in addition to the table relationships
557             above.
558              
559             Note Well: When following the instructions below, never be tempted to
560             use spaces inside column definitions. If you need spaces, colons might
561             work. If not, you'll need to edit the generated bigtop file, just like
562             old times.
563              
564             Column definitions must be placed inside parnetheses immediately after the
565             table name and immediately before any table relationship operator. Separate
566             columns with commas. Specify type definitions with colons. Use equals
567             for defaults and leading plus signs for optional fields. For example:
568              
569             bigtop -n App 'family(name,+phone)<-child(name,birth_day:date)'
570              
571             By default all columns will have type varchar (but note that SQL backends
572             translate that into a valid string type for each supported database,
573             if a bare varchar wouldn't work). If you need some other type, use a colon,
574             as I did for birth_day. If your type definition needs multiple words, use
575             colons instead of spaces.
576              
577             Do not include foreign key columns in the list. They will be generated
578             based on the relationship punctuation between the tables.
579              
580             The phone column in the family table has a leading plus sign, and will
581             therefore be optional on the HTML form.
582              
583             You can still augment the bigtop file later. Existing tables in the bigtop
584             file will have foreign keys added as specified by relation operators, but
585             parenthetical column lists will be used only for new tables. For example:
586              
587             bigtop -a docs/app.bigtop '
588             anniversary(anniversary:date,gift_pref=money)<-family'
589              
590             This will add a new table called anniversary with anniversary (a date) and
591             gift_pref columns. The later will have a default value in the database
592             and on HTML forms of 'money.' Finally, a new foreign key will be added
593             to the existing family table pointing to the anniversary table.
594              
595             You may find it easier to supply the kickstart text by first specifying the
596             relationships without including the columns, then defining the columns later:
597              
598             tentmaker -n App \
599             'child->family anniversary->family
600             child(name,birth_day:date)
601             family(name,+phone)
602             anniversary(anniversary:date,gift_pref=money)'
603              
604             You may mention a table as many times as you like, but only define its
605             columns once.
606              
607             Finally, as mentioned in the L, and described in more detail below
608             (see L), you may put the kickstart in
609             a file and supply the file name on the command line:
610              
611             tentmaker -n App app.kickstart
612              
613             None of the syntax changes when you use the file approach, except that
614             you don't need the shell quotes. In paricular, using a file does not
615             allow you to include spaces within a table's definition.
616              
617             =head2 FORMAL SUMMARY
618              
619             Here is the formal syntax for each table definition:
620              
621             name[(COL_DEF[,COL_DEF...])]
622              
623             I'm following the convention that brackets enclose optional elements.
624             Everything else appears as is, or is defined below.
625              
626             Where name is a valid SQL table name and COL_DEF is as follows:
627              
628             [+]col_name[:TYPE_INFO][=default]
629              
630             Where plus makes the HTML form field for the column optional,
631             col_name is a valid SQL column name, and
632             all defaults are literal strings (they will be quoted in SQL). If you need
633             more interesting defaults, edit the bigtop file after it is updated.
634             TYPE_INFO is a colon separated list of column declaration words.
635              
636             Suppose you want this column definition:
637              
638             state int4 NOT NULL DEFAULT 4,
639              
640             Say this:
641              
642             state:int4:NOT:NULL=4
643              
644             =head2 KICKSTART FILES
645              
646             Traditionally, kickstart text was specified on the command line. Now you
647             can put it in a file and invoke bigtop or tentmaker like this:
648              
649             bigtop -n NewApp file.kickstart
650              
651             Unfortunately, you cannot currently pipe to bigtop or tentmaker, they
652             do not read from standard in.
653              
654             Here is an example kickstart file for a blogging application:
655              
656             blog(active:int4,ident,title,subtitle,blurb,body,gps,comments_enabled:int4,rank:int4,section,username,tag)
657             author(name,address,city,state,country,gps)
658             comment(active:int4,rejected:int4,name,email,url,subject,body)
659             link(active:int4,location,label,posted_date,score,username,tag)
660             tag(active:int4,label,rank)
661             image(active:int4,label,descr,file,default_image,file_ident,file_name,file_size:int4,file_mime,file_suffix)
662             attachment(active:int4,label,descr,file,default_image,file_ident,file_name,file_size:int4,file_mime,file_suffix)
663             section(active:int4,label)
664             blog<-image
665             blog<-attachment
666             blog<-author
667             blog<-comment
668             blog<-section
669              
670             Note again that spaces are not allowed in column definition lists, since
671             whitespace is the separator of table and table relationship entries.
672              
673             =head1 AUTHOR
674              
675             Phil Crow, Ecrow.phil@gmail.comE
676              
677             =head1 COPYRIGHT AND LICENSE
678              
679             Copyright (C) 2007, Phil Crow
680              
681             This library is free software; you can redistribute it and/or modify
682             it under the same terms as Perl itself, either Perl version 5.8.6 or,
683             at your option, any later version of Perl 5 you may have available.
684              
685             =cut
686