File Coverage

blib/lib/Acme/Chef.pm
Criterion Covered Total %
statement 100 111 90.0
branch 18 34 52.9
condition 2 6 33.3
subroutine 13 13 100.0
pod 3 3 100.0
total 136 167 81.4


line stmt bran cond sub pod time code
1             # Please read the pod documentation in this file for
2             # details on how to reach the author and copyright issues.
3              
4             package Acme::Chef;
5              
6 7     7   226811 use 5.006;
  7         104  
  7         342  
7 7     7   38 use strict;
  7         15  
  7         262  
8 7     7   43 use warnings;
  7         13  
  7         217  
9              
10 7     7   35 use Carp;
  7         12  
  7         811  
11              
12 7     7   56 use vars qw/$VERSION/;
  7         14  
  7         437  
13             $VERSION = '1.01';
14              
15 7     7   3975 use Acme::Chef::Recipe;
  7         31  
  7         280  
16 7     7   70 use Acme::Chef::Container;
  7         13  
  7         141  
17 7     7   39 use Acme::Chef::Ingredient;
  7         180  
  7         11084  
18              
19             =head1 NAME
20              
21             Acme::Chef - An interpreter for the Chef programming language
22              
23             =head1 SYNOPSIS
24              
25             # Using the script that comes with the distribution.
26             chef.pl file.chef
27            
28             # Using the module
29             use Acme::Chef;
30            
31             my $compiled = Acme::Chef->compile($code_string);
32             print $compiled->execute();
33            
34             my $string = $compiled->dump(); # requires Data::Dumper
35             # Save it to disk, send it over the web, whatever.
36             my $reconstructed_object = eval $string;
37            
38             # or:
39             $string = $compiled->dump('autorun'); # requires Data::Dumper
40             # Save it to disk, send it over the web, whatever.
41             my $output_of_chef_program = eval $string;
42              
43             =head1 DESCRIPTION
44              
45             Chef is an esoteric programming language in which programs look like
46             recipes. I needn't mention that using it in
47             production environment, heck, using it for anything but entertainment
48             ought to result in bugs and chaos in reverse order.
49              
50             All methods provided by Acme::Chef are adequately described in the
51             synopsis. If you don't think so, you need to read the source code.
52              
53             There has been an update to the Chef specification. I have implemented
54             the changes and marked them in the following documentation with
55             "I".
56              
57             With that out of the way, I would like to present a pod-formatted
58             copy of the Chef specification from David Morgan-Mar's homepage
59             (L).
60              
61             =head2 METHODS
62              
63             This is a list of methods in this package.
64              
65             =over 2
66              
67             =item compile
68              
69             Takes Chef source code as first argument and compiles a Chef program from it.
70             This method doesn't run the code, but returns a program object.
71              
72             =cut
73              
74             sub compile {
75 6     6 1 5069 my $proto = shift;
76 6   33     52 my $class = ref $proto || $proto;
77              
78 6         14 my $code = shift;
79 6 50       24 defined $code or croak "compile takes one argument: a code string.";
80              
81 6         18 my $self = {};
82              
83 6         17 bless $self => $class;
84              
85 6         30 my @paragraphs = $self->_get_paragraphs( $code );
86 6         35 my @recipes = $self->_paragraphsToRecipes(\@paragraphs);
87              
88 6         44 $_->compile() foreach @recipes;
89              
90 6         33 $self->{start_recipe} = $recipes[0]->recipe_name();
91              
92 7         24 $self->{recipes} = {
93 6         16 map { ($_->recipe_name(), $_) } @recipes
94             };
95              
96 6         32 return $self;
97             }
98              
99              
100             =item execute
101            
102             Takes no arguments. Runs the program and returns its output.
103              
104             =cut
105              
106             sub execute {
107 17     17 1 20776 my $self = shift;
108              
109 17         108 my $start_recipe = $self->{recipes}->{ $self->{start_recipe} }->new();
110              
111 17         78 $start_recipe->execute($self->{recipes});
112              
113 17         63 return $start_recipe->output();
114             }
115              
116              
117             =item dump
118            
119             Takes one optional argument. If it equals 'autorun',
120             dump returns a string that, when evaluated, executes
121             the program and returns the output.
122              
123             If the argument does not equal 'autorun', a different
124             string is returned that reconstructs the Acme::Chef
125             object.
126              
127             =cut
128              
129             sub dump {
130 12     12 1 6467 my $self = shift;
131 12         21 my $type = shift;
132 12 100       41 $type = '' if not defined $type;
133              
134 12         19 local $@ = undef;
135 12         6888 require Data::Dumper;
136              
137 12         39618 my $dumper = Data::Dumper->new([$self], ['self']);
138 12         391 $dumper->Indent(0);
139 12         174 $dumper->Purity(1);
140              
141 12         195 my $dump = $dumper->Dump();
142              
143 12 100       8508 if ($type =~ /^autorun$/) {
144 6         61 $dump = 'do{my ' . $dump . ' bless $self => "' . (__PACKAGE__) . '"; $self->execute();} ';
145             } else {
146 6         112 $dump = 'do{my ' . $dump . ' bless $self => "' . (__PACKAGE__) . '";} ';
147             }
148              
149 12         1139 return $dump;
150             }
151              
152              
153             # private function _get_paragraphs
154              
155             sub _get_paragraphs {
156 6     6   13 my $self = shift;
157              
158 6         13 my $string = shift;
159              
160 6         52 $string =~ s/^\s+//;
161 6         262 $string =~ s/\s+$//;
162              
163 6         73 return split /\n{2,}/, $string;
164             }
165              
166              
167             # private function _paragraphsToRecipes
168             #
169             # Constructs recipes from an array ref of paragraphs.
170              
171             sub _paragraphsToRecipes {
172 6     6   14 my $self = shift;
173              
174 6         12 my $paragraphs = shift;
175 6 50 33     110 $paragraphs = shift if not defined $paragraphs or not ref $paragraphs;
176              
177 6         29 while (chomp @$paragraphs) {}
178              
179 6         12 my @recipes;
180              
181 6         13 my $paragraph_no = 0;
182 6         23 while (@$paragraphs) {
183 7         17 my $recipe_name = shift @$paragraphs;
184 7         13 $paragraph_no++;
185 7 50       55 $recipe_name =~ /^[ ]*([\-\w][\- \w]*)\.[ ]*$/
186             or croak "Invalid recipe name specifier in paragraph no. $paragraph_no.";
187 7         40 $recipe_name = lc($1);
188              
189 7 50       33 last unless @$paragraphs;
190 7         15 my $comments = shift @$paragraphs;
191 7         13 $paragraph_no++;
192 7         11 my $ingredients;
193 7 100       38 if ( $comments =~ /^[ ]*Ingredients\.[ ]*\n/ ) {
194 4         7 $ingredients = $comments;
195 4         9 $comments = '';
196             } else {
197 3 50       12 last unless @$paragraphs;
198 3         6 $ingredients = shift @$paragraphs;
199 3         6 $paragraph_no++;
200             }
201              
202 7 50       24 last unless @$paragraphs;
203 7         13 my $cooking_time = shift @$paragraphs;
204 7         17 $paragraph_no++;
205 7         12 my $temperature;
206              
207 7 50       29 if ($cooking_time =~ /^[ ]*Cooking time:[ ]*(\d+)(?: hours?| minutes?)\.[ ]*$/) {
208 0         0 $cooking_time = $1;
209 0 0       0 last unless @$paragraphs;
210 0         0 $temperature = shift @$paragraphs;
211 0         0 $paragraph_no++;
212             } else {
213 7         12 $temperature = $cooking_time;
214 7         14 $cooking_time = '';
215             }
216              
217 7         12 my $method;
218 7 50       31 if ($temperature =~ /^[ ]*Pre-heat oven to (\d+) degrees Celsius(?: gas mark (\d+))?\.[ ]*$/) {
219 0         0 $temperature = $1;
220 0 0       0 $temperature .= ",$2" if defined $2;
221 0 0       0 last unless @$paragraphs;
222 0         0 $method = shift @$paragraphs;
223 0         0 $paragraph_no++;
224             } else {
225 7         274 $method = $temperature;
226 7         15 $temperature = '';
227             }
228              
229 7 50       39 $method =~ /^[ ]*Method\.[ ]*\n/
230             or croak "Invalid method specifier in paragraph no. $paragraph_no.";
231            
232 7         16 my $serves = '';
233 7 100       22 if (@$paragraphs) {
234 4         9 $serves = shift @$paragraphs;
235 4 50       22 if ($serves =~ /^[ ]*Serves (\d+)\.[ ]*$/) {
236 4         11 $serves = $1;
237 4         9 $paragraph_no++;
238             } else {
239 0         0 unshift @$paragraphs, $serves;
240 0         0 $serves = '';
241             }
242             }
243              
244 7         73 push @recipes, Acme::Chef::Recipe->new(
245             name => $recipe_name,
246             comments => $comments,
247             ingredients => $ingredients,
248             cooking_time => $cooking_time,
249             temperature => $temperature,
250             method => $method,
251             serves => $serves,
252             );
253            
254             }
255            
256 6         26 return @recipes;
257             }
258              
259              
260             1;
261             __END__