Commit | Line | Data |
3890b670 |
1 | package MooseX::AutoDoc; |
2 | |
3 | use Moose; |
4 | use Carp; |
5 | use Class::MOP; |
6 | use Moose::Meta::Role; |
7 | use Moose::Meta::Class; |
8 | use Scalar::Util qw/blessed/; |
fcac021f |
9 | use namespace::autoclean; |
3890b670 |
10 | |
11 | # Create a special TypeConstraint for the View so you can just set it |
12 | # with a class name and it'll DWIM |
13 | { |
14 | use Moose::Util::TypeConstraints; |
15 | |
16 | subtype 'AutoDocView' |
17 | => as 'Object' |
18 | => where { $_->isa('MooseX::AutoDoc::View') } |
19 | => message { "Value should be a subclass of MooseX::AutoDoc::View" } ; |
20 | |
21 | coerce 'AutoDocView' |
22 | => from 'Str' |
23 | => via { Class::MOP::load_class($_); $_->new }; |
24 | |
25 | no Moose::Util::TypeConstraints; |
26 | } |
27 | |
28 | #view object |
29 | has view => (is => 'rw', isa => 'AutoDocView', coerce => 1, lazy_build => 1); |
30 | |
31 | #type constraint library to name mapping to make nice links |
32 | has tc_to_lib_map => (is => 'rw', isa => 'HashRef', lazy_build => 1); |
33 | |
34 | #method metaclasses to ignore to avoid documenting some methods |
35 | has ignored_method_metaclasses => (is => 'rw', isa => 'HashRef', lazy_build => 1); |
36 | |
37 | #defaults to artistic... |
38 | has license_text => (is => 'rw', isa => 'Str', lazy_build => 1); |
39 | |
40 | #how can i get the data about the current user? |
41 | has authors => (is => 'rw', isa => 'ArrayRef[HashRef]', |
42 | predicate => 'has_authors'); |
43 | |
44 | sub _build_view { "MooseX::AutoDoc::View::TT" } |
45 | |
46 | sub _build_tc_to_lib_map { |
47 | my %types = map {$_ => 'Moose::Util::TypeConstraints'} |
48 | qw/Any Item Bool Undef Defined Value Num Int Str Role Maybe ClassName Ref |
49 | ScalarRef ArrayRef HashRef CodeRef RegexpRef GlobRef FileHandle Object/; |
50 | return \ %types; |
51 | } |
52 | |
53 | sub _build_ignored_method_metaclasses { |
54 | return { |
3890b670 |
55 | 'Moose::Meta::Method::Accessor' => 1, |
56 | 'Moose::Meta::Method::Constructor' => 1, |
57 | 'Class::MOP::Method::Accessor' => 1, |
58 | 'Class::MOP::Method::Generated' => 1, |
59 | 'Class::MOP::Method::Constructor' => 1, |
60 | }; |
61 | |
ec75fdb0 |
62 | # 'Moose::Meta::Role::Method' => 1, |
3890b670 |
63 | # 'Moose::Meta::Method::Overridden' => 1, |
64 | # 'Class::MOP::Method::Wrapped' => 1, |
ec75fdb0 |
65 | |
3890b670 |
66 | } |
67 | |
68 | sub _build_license_text { |
69 | "This library is free software; you can redistribute it and/or modify it " |
70 | ."under the same terms as Perl itself."; |
71 | } |
72 | |
73 | #make the actual POD |
a3e8bacb |
74 | sub generate_pod_for { |
75 | my ($self, $package, $view_args) = @_; |
3890b670 |
76 | |
a3e8bacb |
77 | carp("${package} is already loaded. This will cause inacurate output.". |
78 | "if ${package} is the consumer of any roles.") |
79 | if Class::MOP::is_class_loaded( $package ); |
3890b670 |
80 | |
10873f5d |
81 | my $spec = $self->_package_info($package); |
a3e8bacb |
82 | my $key = $package->meta->isa("Moose::Meta::Role") ? 'role' : 'class'; |
3890b670 |
83 | my $vars = { |
a3e8bacb |
84 | $key => $spec, |
3890b670 |
85 | license => $self->license_text, |
86 | authors => $self->has_authors ? $self->authors : [], |
87 | }; |
a3e8bacb |
88 | my $render = "render_${key}"; |
89 | return $self->view->$render($vars, $view_args); |
3890b670 |
90 | } |
91 | |
3890b670 |
92 | # *_info methods |
a3e8bacb |
93 | sub _package_info { |
94 | my($self, $package) = @_; |
95 | |
96 | #intercept role application so we can accurately generate |
97 | #method and attribute information for the parent class. |
98 | #this is fragile, but there is not better way that i am aware of |
99 | my $rmeta = Moose::Meta::Role->meta; |
100 | $rmeta->make_mutable if $rmeta->is_immutable; |
101 | my $original_apply = $rmeta->get_method("apply")->body; |
102 | $rmeta->remove_method("apply"); |
103 | my @roles_to_apply; |
104 | $rmeta->add_method("apply", sub{push(@roles_to_apply, [@_])}); |
105 | #load the package with the hacked Moose::Meta::Role |
106 | eval { Class::MOP::load_class($package); }; |
107 | confess "Failed to load package ${package} $@" if $@; |
108 | |
109 | #get on with analyzing the package |
110 | my $meta = $package->meta; |
111 | my $spec = {}; |
112 | my ($class, $is_role); |
113 | if($package->meta->isa('Moose::Meta::Role')){ |
114 | $is_role = 1; |
115 | # we need to apply the role to a class to be able to properly introspect it |
116 | $class = Moose::Meta::Class->create_anon_class; |
117 | $original_apply->($meta, $class); |
118 | } else { |
119 | #roles don't have superclasses ... |
120 | $class = $meta; |
121 | my @superclasses = map{ $_->meta } |
122 | grep { $_ ne 'Moose::Object' } $meta->superclasses; |
123 | my @superclass_specs = map{ $self->_superclass_info($_) } @superclasses; |
124 | $spec->{superclasses} = \@superclass_specs; |
3890b670 |
125 | } |
126 | |
a3e8bacb |
127 | #these two are common to both roles and classes |
128 | my @attributes = map{ $class->get_attribute($_) } sort $class->get_attribute_list; |
129 | my @methods = |
3890b670 |
130 | grep{ ! exists $self->ignored_method_metaclasses->{$_->meta->name} } |
a3e8bacb |
131 | map { $class->get_method($_) } |
132 | grep { $_ ne 'meta' } sort $class->get_method_list; |
133 | |
134 | my @method_specs = map{ $self->_method_info($_) } @methods; |
135 | my @attribute_specs = map{ $self->_attribute_info($_) } @attributes; |
136 | |
137 | #fix Moose::Meta::Role and apply the roles that were delayed |
138 | $rmeta->remove_method("apply"); |
139 | $rmeta->add_method("apply", $original_apply); |
140 | $rmeta->make_immutable; |
141 | #we apply roles to be able to figure out which ones we are using although I |
142 | #could just cycle through $_->[0] for @roles_to_apply; |
143 | shift(@$_)->apply(@$_) for @roles_to_apply; |
144 | |
145 | #Moose::Meta::Role and Class have different methods to get consumed roles.. |
146 | #make sure we break up composite roles as well to get better names and nicer |
147 | #linking to packages. |
3890b670 |
148 | my @roles = sort{ $a->name cmp $b->name } |
a3e8bacb |
149 | map { $_->isa("Moose::Meta::Role::Composite") ? @{ $_->get_roles } : $_ } |
150 | @{ $is_role ? $meta->get_roles : $meta->roles }; |
ec75fdb0 |
151 | my @role_specs = map{ $self->_consumed_role_info($_) } @roles; |
3890b670 |
152 | |
a3e8bacb |
153 | #fill up the spec |
154 | $spec->{name} = $meta->name; |
155 | $spec->{roles} = \ @role_specs; |
156 | $spec->{methods} = \ @method_specs; |
157 | $spec->{attributes} = \ @attribute_specs; |
3890b670 |
158 | |
159 | return $spec; |
160 | } |
161 | |
ec75fdb0 |
162 | sub _attribute_info{ |
3890b670 |
163 | my($self, $attr) = @_;; |
164 | my $attr_name = $attr->name; |
165 | my $spec = { name => $attr_name }; |
166 | my $info = $spec->{info} = {}; |
167 | |
168 | $info->{clearer} = $attr->clearer if $attr->has_clearer; |
169 | $info->{builder} = $attr->builder if $attr->has_builder; |
170 | $info->{predicate} = $attr->predicate if $attr->has_predicate; |
171 | |
172 | |
173 | my $description = $attr->is_required ? 'Required ' : 'Optional '; |
174 | if( defined(my $is = $attr->_is_metadata) ){ |
175 | $description .= 'read-only ' if $is eq 'ro'; |
176 | $description .= 'read-write ' if $is eq 'rw'; |
177 | |
178 | #If we have 'is' info only write out this info if it != attr_name |
179 | $info->{writer} = $attr->writer |
180 | if $attr->has_writer && $attr->writer ne $attr_name; |
181 | $info->{reader} = $attr->reader |
182 | if $attr->has_reader && $attr->reader ne $attr_name; |
183 | $info->{accessor} = $attr->accessor |
184 | if $attr->has_accessor && $attr->accessor ne $attr_name; |
185 | } else { |
186 | $info->{writer} = $attr->writer if $attr->has_writer; |
187 | $info->{reader} = $attr->reader if $attr->has_reader; |
188 | $info->{accessor} = $attr->accessor if $attr->has_accessor; |
189 | } |
a8676f0f |
190 | $info->{'constructor key'} = $attr->init_arg |
191 | if $attr->has_init_arg && $attr->init_arg ne $attr_name; |
3890b670 |
192 | |
193 | if( defined(my $lazy = $attr->is_lazy) ){ |
194 | $description .= 'lazy-building '; |
195 | } |
196 | $description .= 'value'; |
197 | if( defined(my $isa = $attr->_isa_metadata) ){ |
198 | my $link_to; |
199 | if( blessed $isa ){ |
200 | my $from_type_lib; |
201 | while( blessed $isa ){ |
202 | $isa = $isa->name; |
203 | } |
204 | my @parts = split '::', $isa; |
205 | my $type_name = pop @parts; |
206 | my $type_lib = join "::", @parts; |
207 | if(eval{$type_lib->isa("MooseX::Types::Base")}){ |
208 | $link_to = $type_lib; |
209 | $isa = $type_name; |
210 | } |
211 | } else { |
212 | my ($isa_base) = ($isa =~ /^(.*?)(?:\[.*\])?$/); |
213 | if (exists $self->tc_to_lib_map->{$isa_base}){ |
214 | $link_to = $self->tc_to_lib_map->{$isa_base}; |
215 | } |
216 | my $isa = $isa_base; |
217 | } |
218 | if(defined $link_to){ |
219 | $isa = "L<${isa}|${link_to}>"; |
220 | } |
221 | $description .= " of type ${isa}"; |
222 | } |
223 | if( $attr->should_auto_deref){ |
224 | $description .=" that will be automatically dereferenced by ". |
225 | "the reader / accessor"; |
226 | } |
227 | if( $attr->has_documentation ){ |
228 | $description .= "\n\n" . $attr->documentation; |
229 | } |
230 | $spec->{description} = $description; |
231 | |
232 | return $spec; |
233 | } |
234 | |
ec75fdb0 |
235 | sub _superclass_info { |
3890b670 |
236 | my($self, $superclass) = @_; |
237 | my $spec = { name => $superclass->name }; |
238 | return $spec; |
239 | } |
240 | |
ec75fdb0 |
241 | sub _method_info { |
3890b670 |
242 | my($self, $method) = @_; |
243 | my $spec = { name => $method->name }; |
244 | return $spec; |
245 | } |
246 | |
ec75fdb0 |
247 | sub _consumed_role_info { |
3890b670 |
248 | my($self, $role) = @_;; |
249 | my $spec = { name => $role->name }; |
250 | return $spec; |
251 | } |
252 | |
253 | 1; |
254 | |
255 | __END__; |
256 | |
ec75fdb0 |
257 | =head1 NAME |
258 | |
a3e8bacb |
259 | MooseX::AutoDoc - Automatically generate documentation for Moose-based packages |
ec75fdb0 |
260 | |
0fe5ef1b |
261 | =head1 SYNOPSIS |
ec75fdb0 |
262 | |
263 | use MooseX::AutoDoc; |
264 | my $autodoc = MooseX::AutoDoc->new |
265 | ( |
266 | authors => |
267 | [ |
268 | { |
269 | name => "Guillermo Roditi", |
270 | email => 'groditi@cpan.org', |
271 | handle => "groditi", |
272 | } |
273 | ], |
274 | ); |
275 | |
a3e8bacb |
276 | my $class_pod = $autodoc->generate_pod_for("MyClass"); |
277 | my $role_pod = $autodoc->generate_pod_for("MyRole"); |
ec75fdb0 |
278 | |
279 | =head1 DESCRIPTION |
280 | |
281 | MooseX::AutoDoc allows you to automatically generate POD documentation from |
a3e8bacb |
282 | your Moose based objects by introspecting them and creating a POD skeleton |
283 | with extra information where it can be infered through the MOP. |
ec75fdb0 |
284 | |
285 | =head1 NOTICE REGARDING ROLE CONSUMPTION |
286 | |
287 | To accurantely detect which methods and attributes are part of the class / role |
a3e8bacb |
288 | being examined and which are part of a consumed role the L</"generate_pod_for"> |
289 | method need to delay role consumption. If your role or class has been loaded |
290 | prior to calling these methods you run a risk of recieving inacurate data and |
291 | a warning will be emitted. This is due to the fact that once a role is applied |
292 | there is no way to tell which attributes and methods came from the class and |
293 | which came from the role. |
ec75fdb0 |
294 | |
295 | =head1 ATTRIBUTES |
296 | |
297 | Unless noted otherwise, you may set any of these attributes at C<new> time by |
298 | passing key / value pairs to C<new> where the key is the name of the attribute |
299 | you wish to set. Unless noted otherwise accessor methods for attributes also |
300 | share the same name as the attribute. |
301 | |
302 | =head2 authors |
303 | |
304 | =over 4 |
305 | |
306 | =item B<predicate> - has_authors |
307 | |
308 | =back |
309 | |
310 | Optional read-write value of type |
311 | L<ArrayRef[HashRef]|Moose::Util::TypeConstraints> representing the authors of |
312 | the class / role being documented. These values are passed directly to the view |
313 | and the default TT view accepts entries in the following form |
314 | (all fields optional) |
315 | |
316 | { |
317 | name => 'Guillermo Roditi', |
318 | handle => 'groditi', |
319 | email => '<groditi@gmail.com>', |
320 | } |
321 | |
322 | =head2 ignored_method_metaclasses |
323 | |
324 | =over 4 |
325 | |
326 | =item B<builder> - _build_ignored_method_metaclasses |
327 | |
328 | Default to the Moose and Class::MOP method metaclasses for generated methods, |
329 | accessors, and constructors. |
330 | |
331 | =item B<clearer> - clear_ignored_method_metaclasses |
332 | |
333 | =item B<predicate> - has_ignored_method_metaclasses |
334 | |
335 | =back |
336 | |
337 | Required read-write lazy-building value of type |
338 | L<HashRef|Moose::Util::TypeConstraints> where the keys are method metaclasses |
339 | MooseX::AutoDoc should ignore when creating a method list. |
340 | |
341 | =head2 license_text |
342 | |
343 | =over 4 |
344 | |
345 | =item B<builder> - _build_license_text |
346 | |
347 | =item B<clearer> - clear_license_text |
348 | |
349 | =item B<predicate> - has_license_text |
350 | |
351 | =back |
352 | |
353 | Required read-write lazy-building value of type |
354 | L<Str|Moose::Util::TypeConstraints>. By default it will use the following text: |
355 | |
356 | This library is free software; you can redistribute it and/or modify it |
357 | under the same terms as Perl itself. |
358 | |
359 | =head2 tc_to_lib_map |
360 | |
361 | =over 4 |
362 | |
363 | =item B<builder> - _build_tc_to_lib_map |
364 | |
365 | =item B<clearer> - clear_tc_to_lib_map |
366 | |
367 | =item B<predicate> - has_tc_to_lib_map |
368 | |
369 | =back |
370 | |
371 | Required read-write lazy-building value of type |
372 | L<HashRef|Moose::Util::TypeConstraints>. The keys refer to type constraint |
373 | names and the values to the module where the documentation available for that |
374 | type is. Please note that if you are using MooseX::Types libraries the links |
375 | will be automatically generated if the library class can be found (most cases). |
376 | |
377 | =head2 view |
378 | |
379 | =over 4 |
380 | |
381 | =item B<builder> - _build_view |
382 | |
383 | Returns 'MooseX::AutoDoc::View::TT' |
384 | |
385 | =item B<clearer> - clear_view |
386 | |
387 | =item B<predicate> - has_view |
388 | |
389 | =back |
390 | |
391 | Required read-write lazy-building value of type AutoDocView. The AutoDocView |
392 | type will accept an Object that isa L<MooseX::AutoDoc::View>. This attribute |
393 | will attempt to coerce string values to instances by treating them as class |
394 | names and attempting to load and instantiate a class of the same name. |
395 | |
396 | =head1 METHODS |
397 | |
9afe15bd |
398 | =head2 new key => $value |
ec75fdb0 |
399 | |
400 | Instantiate a new object. Please refer to L</"ATTRIBUTES"> for a list of valid |
401 | key options. |
402 | |
a3e8bacb |
403 | =head2 generate_pod_for $package_name, $view_args |
ec75fdb0 |
404 | |
a3e8bacb |
405 | Returns a string containing the Pod for the package. To make sure the data is |
406 | accurate please make sure the package has not been loaded prior to this step. |
ec75fdb0 |
407 | for more info see L</"NOTICE REGARDING ROLE CONSUMPTION"> |
408 | |
a3e8bacb |
409 | =head2 _package_info $package_name |
ec75fdb0 |
410 | |
a3e8bacb |
411 | Will return a hashref representing the documentation components of the package |
412 | with the keys C<name>, C<attributes>, C<methods>, C<attributes> and--if the |
413 | case the package is a class--C<superclasses>; the latter four are array refs |
414 | of the hashrefs returned by L</"_superclass_info">, L</"_attribute_info">, |
415 | L</"_method_info">, and L</"_consumed_role_info"> respectively. |
ec75fdb0 |
416 | |
417 | =head2 _attribute_info $attr |
418 | |
419 | Accepts one argument, an attribute metaclass instance. |
420 | Returns a hashref representing the documentation components of the |
421 | attribute with the keys C<name>, C<description>, and C<info>, a hashref |
a3e8bacb |
422 | of additional information. If you have set the documentation attribute of |
423 | your attributes the documentation text will be appended to the auto-generated |
424 | description. |
ec75fdb0 |
425 | |
426 | =head2 _consumed_role_info $role |
427 | |
428 | Accepts one argument, a role metaclass instance. Returns a hashref representing |
429 | the documentation components of the role with the key C<name>. |
430 | |
431 | =head2 _method_info $method |
432 | |
433 | Accepts one argument, a method metaclass instance. Returns a hashref |
434 | representing the documentation components of the role with the key C<name>. |
435 | |
436 | =head2 _superclass_info $class |
437 | |
438 | Accepts one argument, the metaclass instance of a superclass. Returns a hashref |
439 | representing the documentation components of the role with the key C<name>. |
440 | |
441 | =head2 meta |
442 | |
443 | Retrieve the metaclass instance. Please see L<Moose::Meta::Class> and |
444 | L<Class::MOP::Class> for more information. |
445 | |
446 | =head1 AUTHORS |
447 | |
448 | Guillermo Roditi (Guillermo Roditi) <groditi@cpan.org> |
449 | |
450 | =head1 COPYRIGHT AND LICENSE |
3890b670 |
451 | |
ec75fdb0 |
452 | This library is free software; you can redistribute it and/or modify it under |
453 | the same terms as Perl itself. |
3890b670 |
454 | |
ec75fdb0 |
455 | =cut |