Commit | Line | Data |
7adfd53f |
1 | package Reaction::UI::WidgetClass; |
2 | |
3 | use Reaction::ClassExporter; |
4 | use Reaction::Class; |
5 | use Reaction::UI::Widget; |
6 | use Data::Dumper; |
7 | |
8 | no warnings 'once'; |
9 | |
10 | class WidgetClass, which { |
11 | |
12 | overrides exports_for_package => sub { |
13 | my ($self, $package) = @_; |
14 | return (super(), |
15 | func => sub { |
16 | my ($k, $m) = @_; |
17 | my $sig = "should be: func(data_key => 'method_name')"; |
18 | confess "Data key not present, ${sig}" unless defined($k); |
19 | confess "Data key must be string, ${sig}" unless !ref($k); |
20 | confess "Method name not present, ${sig}" unless defined($m); |
21 | confess "Method name must be string, ${sig}" unless !ref($m); |
22 | [ $k, $m ]; |
23 | }, # XXX zis is not ze grand design. OBSERVABLE. |
24 | string => sub (&) { -string => [ @_ ] }, # meh (maybe &;@ later?) |
25 | wrap => sub { $self->do_wrap_sub($package, @_); }, # should have class. |
26 | ); |
27 | }; |
28 | |
29 | overrides default_base => sub { ('Reaction::UI::Widget') }; |
30 | |
31 | overrides do_class_sub => sub { |
32 | my ($self, $package, $class) = @_; |
33 | # intercepts 'foo renders ...' |
34 | local *renders::AUTOLOAD = sub { |
35 | our $AUTOLOAD; |
36 | shift; |
37 | $AUTOLOAD =~ /^renders::(.*)$/; |
38 | $self->do_renders_meth($package, $class, $1, @_); |
39 | }; |
40 | # intercepts 'foo over ...' |
41 | local *over::AUTOLOAD = sub { |
42 | our $AUTOLOAD; |
43 | shift; |
44 | $AUTOLOAD =~ /^over::(.*)$/; |
45 | $self->do_over_meth($package, $class, $1, @_); |
46 | }; |
47 | # $_ returns '-topic:_', $_{foo} returns '-topic:foo' |
48 | local $_ = '-topic:_'; |
49 | my %topichash; |
50 | tie %topichash, 'Reaction::UI::WidgetClass::TopicHash'; |
51 | local *_ = \%topichash; |
52 | super; |
53 | }; |
54 | |
55 | implements do_wrap_sub => as { confess "Unimplemented" }; |
56 | |
57 | implements do_renders_meth => as { |
58 | my ($self, $package, $class, $fname, $content, $args, $extra) = @_; |
59 | |
60 | my $sig = 'should be: renders [ <content spec> ], \%args?'; |
61 | |
62 | confess "Too many args to renders, ${sig}" if defined($extra); |
63 | confess "First arg not an arrayref, ${sig}" unless ref($content) eq 'ARRAY'; |
64 | confess "Args must be hashref, ${sig}" |
65 | if (defined($args) && (ref($args) ne 'HASH')); |
66 | |
67 | $sig .= ' |
68 | where content spec is [ fragment_name over func(...), \%args? ] |
69 | or [ qw(list of fragment names), \%args ]'; # explain the mistake, yea |
70 | |
71 | my $inner_args = ((ref($content->[-1]) eq 'HASH') ? pop(@$content) : {}); |
72 | # [ blah over func(...), { ... } ] or [ qw(foo bar), { ... } ] |
73 | |
74 | # predeclare since content_gen gets populated somewhere in an if |
75 | # and inner_args_gen wants to be closed over by content_gen |
76 | |
77 | my ($content_gen, $inner_args_gen); |
78 | |
79 | my %args_extra; # again populated (possibly) within the if |
80 | |
81 | confess "Content spec invalid, ${sig}" |
82 | unless defined($content->[0]) && !ref($content->[0]); |
83 | |
84 | if (my ($key) = ($content->[0] =~ /^-(.*)?/)) { |
85 | |
86 | # if first content value is -foo, pull it off the front and then |
87 | # figure out is it's a type we know how to handle |
88 | |
89 | shift(@$content); |
90 | if ($key eq 'over') { # fragment_name over func |
91 | my ($fragment, $func) = @$content; |
92 | confess "Fragment name invalid, ${sig}" if ref($fragment); |
93 | my $content_meth = "render_${fragment}"; |
94 | # grab result of func |
95 | # - if arrayref, render fragment per entry |
96 | # - if obj and can('next') call that until undef |
97 | # - else scream loudly |
98 | my ($func_key, $func_meth) = @$func; |
99 | $content_gen = sub { |
100 | my ($widget, $args) = @_; |
101 | my $topic = eval { $args->{$func_key}->$func_meth }; |
102 | confess "Error calling ${func_meth} on ${func_key} argument " |
103 | .($args->{$func_key}||'').": $@" |
104 | if $@; |
105 | my $iter_sub; |
106 | if (ref $topic eq 'ARRAY') { |
107 | my @copy = @$topic; # non-destructive on original data |
108 | $iter_sub = sub { shift(@copy); }; |
109 | } elsif (Scalar::Util::blessed($topic) && $topic->can('next')) { |
110 | $iter_sub = sub { $topic->next }; |
111 | } else { |
112 | #confess "func(${func_key} => ${func_meth}) for topic within fragment ${fname} did not return arrayref or iterator object"; |
113 | # Coercing to a single-arg list instead for the mo. Mistake? |
114 | my @copy = ($topic); |
115 | $iter_sub = sub { shift(@copy); }; |
116 | } |
117 | my $inner_args = $inner_args_gen->($args); |
118 | return sub { |
119 | my $next = $iter_sub->(); |
120 | return undef unless $next; |
121 | return sub { |
122 | my ($rctx) = @_; |
123 | local $inner_args->{'_'} = $next; # ala local $_, why copy? |
124 | $widget->$content_meth($rctx, $inner_args); |
125 | }; |
126 | }; |
127 | }; |
128 | } elsif ($key eq 'string') { |
129 | |
130 | # string { ... } |
131 | |
132 | my $sub = $content->[0]->[0]; # string {} returns (-string => [ $cr ]) |
133 | $content_gen = sub { |
134 | my ($widget, $args) = @_; |
135 | my $done = 0; |
136 | my $inner_args = $inner_args_gen->($args); |
137 | return sub { |
138 | return if $done++; # a string content only happens once |
139 | return sub { # setup $_{foo} etc. and alias $_ to $_{_} |
140 | my ($rctx) = @_; |
141 | local *_ = \%{$inner_args}; |
142 | local $_ = $inner_args->{'_'}; |
143 | $sub->($rctx); |
144 | }; |
145 | }; |
146 | }; |
147 | |
148 | # must also handle just $_ later for wrap |
149 | } else { |
150 | # unrecognised -foo |
151 | confess "Unrecognised content spec type ${key}, ${sig}"; |
152 | } |
153 | } else { |
154 | |
155 | # handling the renders [ qw(list of frag names), \%args ] case |
156 | |
157 | #warn @$content; |
158 | confess "Invalid content spec, ${sig}" |
159 | if grep { ref($_) } @$content; |
160 | $content_gen = sub { |
161 | my ($widget, $args) = @_; |
162 | my @fragment_methods = map { "render_${_}" } @$content; |
163 | my $inner_args = $inner_args_gen->($args); |
164 | return sub { |
165 | my $next = shift(@fragment_methods); |
166 | return undef unless $next; |
167 | return sub { |
168 | my ($rctx) = @_; |
169 | $widget->$next($rctx, $inner_args); |
170 | }; |
171 | }; |
172 | }; |
173 | |
174 | foreach my $key (@$content) { |
175 | my $frag_meth = "render_${key}"; |
176 | $args_extra{$key} = sub { |
177 | my ($widget, $args) = @_; |
178 | my $inner_args = $inner_args_gen->($args); |
179 | return sub { |
180 | my ($rctx) = @_; |
181 | $widget->$frag_meth($rctx, $inner_args); |
182 | }; |
183 | }; |
184 | } |
185 | } |
186 | |
187 | # populate both args generators here primarily for clarity |
188 | |
189 | my $args_gen = $self->mk_args_generator($args); |
190 | $inner_args_gen = $self->mk_args_generator($inner_args); |
191 | |
192 | my $methname = "render_${fname}"; |
193 | |
194 | $args_extra{'_'} = $content_gen; |
195 | |
196 | my @extra_keys = keys %args_extra; |
197 | my @extra_gen = values %args_extra; |
198 | |
199 | my $meth = sub { |
200 | my ($self, $rctx, $args) = @_; |
201 | confess "No rendering context passed" unless $rctx; |
202 | my $r_args = $args_gen->($args); |
203 | #warn Dumper($r_args).' '; |
204 | @{$r_args}{@extra_keys} = map { $_->($self, $args); } @extra_gen; |
205 | $r_args->{'_'} = $content_gen->($self, $args); |
206 | #warn Dumper($r_args).' '; |
207 | $rctx->render($fname, $r_args); |
208 | }; |
209 | |
210 | $class->meta->add_method($methname => $meth); |
211 | }; |
212 | |
213 | implements do_over_meth => as { |
214 | my ($self, $package, $class, @args) = @_; |
215 | #warn Dumper(\@args); |
216 | return (-over => @args); |
217 | }; |
218 | |
219 | implements mk_args_generator => as { |
220 | my ($self, $argspec) = @_; |
221 | #warn Dumper($argspec); |
222 | # only handling [ $k, $v ] (func()) and -topic:$x ($_{$x}) for the moment |
223 | |
224 | my $sig = 'should be: key => $_ or key => $_{name} or key => func("name", "method")'; |
225 | |
226 | my (@func_to, @func_spec, @copy_from, @copy_to); |
227 | foreach my $key (keys %$argspec) { |
228 | my $val = $argspec->{$key}; |
229 | if (ref($val) eq 'ARRAY') { |
230 | push(@func_spec, $val); |
231 | push(@func_to, $key); |
232 | } elsif (!ref($val) && ($val =~ /^-topic:(.*)$/)) { |
233 | my $topic_key = $1; |
234 | push(@copy_from, $topic_key); |
235 | push(@copy_to, $key); |
236 | } else { |
237 | confess "Invalid args member for ${key}, ${sig}"; |
238 | } |
239 | } |
240 | #warn Dumper(\@func_to, \@func_spec, \@copy_from, \@copy_to); |
241 | return sub { |
242 | my ($outer_args) = @_; |
243 | my $args = { %$outer_args }; |
244 | #warn Dumper(\@func_to, \@func_spec, \@copy_from, \@copy_to).' '; |
245 | @{$args}{@copy_to} = @{$outer_args}{@copy_from}; |
246 | @{$args}{@func_to} = (map { |
247 | my ($key, $meth) = @{$_}; |
248 | $outer_args->{$key}->$meth; # [ 'a, 'b' ] ~~ ->{'a'}->b |
249 | } @func_spec); |
250 | #warn Dumper($args).' '; |
251 | return $args; |
252 | }; |
253 | }; |
254 | |
255 | }; |
256 | |
257 | package Reaction::UI::WidgetClass::TopicHash; |
258 | |
259 | use Tie::Hash; |
260 | use base qw(Tie::StdHash); |
261 | |
262 | sub FETCH { |
263 | my ($self, $key) = @_; |
264 | return "-topic:${key}"; |
265 | } |
266 | |
267 | 1; |
268 | |
269 | =head1 NAME |
270 | |
271 | Reaction::UI::WidgetClass |
272 | |
273 | =head1 DESCRIPTION |
274 | |
275 | =head1 AUTHORS |
276 | |
277 | See L<Reaction::Class> for authors. |
278 | |
279 | =head1 LICENSE |
280 | |
281 | See L<Reaction::Class> for the license. |
282 | |
283 | =cut |