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