File Coverage

lib/UR/Object/View/Default/Text.pm
Criterion Covered Total %
statement 85 97 87.6
branch 22 30 73.3
condition 8 9 88.8
subroutine 8 10 80.0
pod 1 3 33.3
total 124 149 83.2


line stmt bran cond sub pod time code
1             package UR::Object::View::Default::Text;
2              
3 3     3   112 use strict;
  3         3  
  3         75  
4 3     3   9 use warnings;
  3         6  
  3         2414  
5             require UR;
6             our $VERSION = "0.46"; # UR $VERSION;
7              
8             class UR::Object::View::Default::Text {
9             is => 'UR::Object::View',
10             has_constant => [
11             perspective => { value => 'default' },
12             toolkit => { value => 'text' },
13             ],
14             has => [
15             indent_text => { is => 'Text', default_value => ' ', doc => 'indent child views with this text' },
16             ],
17             };
18              
19             # general view API
20              
21             sub _create_widget {
22             # The "widget" for a text view is a pair of items:
23             # - a scalar reference to hold the content
24             # - an I/O handle to which it will display (the "window" it lives in)
25              
26             # Note that the former could be something tied to an object,
27             # a file, or other external storage, though it is
28             # simple by default. The later might also be tied.
29            
30             # The later is STDOUT unless overridden/changed.
31 47     47   67 my $self = shift;
32 47         69 my $scalar_ref = '';
33 47         65 my $fh = 'STDOUT';
34 47         127 return [\$scalar_ref,$fh];
35             }
36              
37             sub show {
38             # Showing a text view typically prints to STDOUT
39 1     1 1 2 my $self = shift;
40 1         11 my $widget = $self->widget();
41 1         2 my ($content_ref,$output_stream) = @$widget;
42 1         19 $output_stream->print($$content_ref,"\n");
43             }
44              
45             sub _update_subject_from_view {
46 0     0   0 Carp::confess('currently text views are read-only!');
47             }
48              
49             sub _update_view_from_subject {
50 133     133   157 my $self = shift;
51 133         483 my $content = $self->_generate_content(@_);
52 133         427 my $widget = $self->widget();
53 133         190 my ($content_ref,$fh) = @$widget;
54 133         142 $$content_ref = $content;
55 133         206 return 1;
56             }
57              
58             # text view API
59              
60             sub content {
61             # retuns the current value of the scalar ref containing the text content.
62 95     95 0 2916 my $self = shift;
63              
64 95         210 my $widget = $self->widget();
65 95 50       177 if (@_) {
66 0         0 die "the widget reference for a view isn't changeable. change its content..";
67             }
68 95         168 my ($content_ref,$output_stream) = @$widget;
69 95         209 return $$content_ref;
70             }
71              
72             sub output_stream {
73             # retuns the current value of the handle to which we render.
74 0     0 0 0 my $self = shift;
75 0         0 my $widget = $self->widget();
76 0 0       0 if (@_) {
77 0         0 return $widget->[1] = shift;
78             }
79 0         0 my ($content_ref,$output_stream) = @$widget;
80 0         0 return $output_stream;
81             }
82              
83             sub _generate_content {
84 22     22   33 my $self = shift;
85              
86             # the header line is the class followed by the id
87 22         61 my $text = $self->subject_class_name;
88 22         71 $text =~ s/::/ /g;
89 22         55 my $subject = $self->subject();
90 22 50       55 if ($subject) {
91 22         55 my $subject_id_txt = $subject->id;
92 22 50       88 $subject_id_txt = "'$subject_id_txt'" if $subject_id_txt =~ /\s/;
93 22         51 $text .= " $subject_id_txt";
94             }
95              
96             # Don't recurse back into something we're already in the process of showing
97 22 100       91 if ($self->_subject_is_used_in_an_encompassing_view()) {
98 7         16 $text .= " (REUSED ADDR)\n";
99             } else {
100 15         30 $text .= "\n";
101             # the content for any given aspect is handled separately
102 15         49 my @aspects = $self->aspects;
103 63         74 my @sorted_aspects = map { $_->[1] }
104 89         90 sort { $a->[0] <=> $b->[0] }
105 15         41 map { [ $_->number, $_ ] }
  63         92  
106             @aspects;
107 15         45 for my $aspect (@sorted_aspects) {
108 63 100       174 next if $aspect->name eq 'id';
109 54         176 my $aspect_text = $self->_generate_content_for_aspect($aspect);
110 54         160 $text .= $aspect_text;
111             }
112             }
113              
114 22         62 return $text;
115             }
116              
117             sub _generate_content_for_aspect {
118             # This does two odd things:
119             # 1. It gets the value(s) for an aspect, then expects to just print them
120             # unless there is a delegate view. In which case, it replaces them
121             # with the delegate's content.
122             # 2. In cases where more than one value is returned, it recycles the same
123             # view and keeps the content.
124             #
125             # These shortcuts make it hard to abstract out logic from toolkit-specifics
126              
127 54     54   68 my $self = shift;
128 54         60 my $aspect = shift;
129              
130 54         149 my $subject = $self->subject;
131 54         187 my $indent_text = $self->indent_text;
132              
133 54         180 my $aspect_text = $indent_text . $aspect->label . ": ";
134              
135 54 50       106 if (!$subject) {
136 0         0 $aspect_text .= "-\n";
137 0         0 return $aspect_text;
138             }
139              
140 54         110 my $aspect_name = $aspect->name;
141              
142 54         55 my @value;
143 54         69 eval {
144 54         261 @value = $subject->$aspect_name;
145             };
146              
147 54 100       117 if (@value == 0) {
148 6         15 $aspect_text .= "-\n";
149 6         19 return $aspect_text;
150             }
151              
152 48 50 66     220 if (@value == 1 and ref($value[0]) eq 'ARRAY') {
153 0         0 @value = @{$value[0]};
  0         0  
154             }
155              
156 48 100       135 unless ($aspect->delegate_view) {
157 28         105 $aspect->generate_delegate_view;
158             }
159              
160             # Delegate to a subordinate view if needed.
161             # This means we replace the value(s) with their
162             # subordinate widget content.
163 48 50       122 if (my $delegate_view = $aspect->delegate_view) {
164             # TODO: it is bad to recycle a view here??
165             # Switch to a set view, which is the standard lister.
166 48         75 foreach my $value ( @value ) {
167 56 100       137 if (Scalar::Util::blessed($value)) {
168 18         57 $delegate_view->subject($value);
169             }
170             else {
171 38         143 $delegate_view->subject_id($value);
172             }
173 56         162 $delegate_view->_update_view_from_subject();
174 56         142 $value = $delegate_view->content();
175             }
176             }
177              
178 48 100 100     324 if (@value == 1 and defined($value[0]) and index($value[0],"\n") == -1) {
      100        
179             # one item, one row in the value or sub-view of the item:
180 35         75 $aspect_text .= $value[0] . "\n";
181             }
182             else {
183 13         16 my $aspect_indent;
184 13 100       34 if (@value == 1) {
185             # one level of indent for this sub-view's sub-aspects
186             # zero added indent for the identity line b/c it's next-to the field label
187              
188             # aspect1: class with id ID
189             # sub-aspect1: value1
190             # sub-aspect2: value2
191 9         14 $aspect_indent = $indent_text;
192             }
193             else {
194             # two levels of indent for this sub-view's sub-aspects
195             # just one level for each identity
196              
197             # aspect1: ...
198             # class with id ID
199             # sub-aspect1: value1
200             # sub-aspect2: value2
201             # class with id ID
202             # sub-aspect1: value1
203             # sub-aspect2: value2
204 4         9 $aspect_text .= "\n";
205 4         6 $aspect_indent = $indent_text . $indent_text;
206             }
207              
208 13         24 for my $value (@value) {
209 21         26 my $value_indented = '';
210 21 100       40 if (defined $value) {
211 18         44 my @rows = split(/\n/,$value);
212 18         25 $value_indented = join("\n", map { $aspect_indent . $_ } @rows);
  44         75  
213 18         32 chomp $value_indented;
214             }
215 21         44 $aspect_text .= $value_indented . "\n";
216             }
217             }
218 48         131 return $aspect_text;
219             }
220              
221             1;
222              
223              
224             =pod
225              
226             =head1 NAME
227              
228             UR::Object::View::Default::Text - object views in text format
229              
230             =head1 SYNOPSIS
231              
232             $o = Acme::Product->get(1234);
233              
234             # generates a UR::Object::View::Default::Text object:
235             $v = $o->create_view(
236             toolkit => 'text',
237             aspects => [
238             'id',
239             'name',
240             'qty_on_hand',
241             'outstanding_orders' => [
242             'id',
243             'status',
244             'customer' => [
245             'id',
246             'name',
247             ]
248             ],
249             ],
250             );
251              
252              
253             $txt1 = $v->content;
254              
255             $o->qty_on_hand(200);
256            
257             $txt2 = $v->content;
258              
259              
260             =head1 DESCRIPTION
261              
262             This class implements basic text views of objects. It is used for command-line tools,
263             and is the base class for other specific text formats like XML, HTML, JSON, etc.
264              
265             =head1 WRITING A SUBCLASS
266              
267             # In Acme/Product/View/OutstandingOrders/Text.pm
268              
269             package Acme::Product::View::OutstandingOrders::Text;
270             use UR;
271              
272             class Acme::Product::View::OutstandingOrders::Text {
273             is => 'UR::Object::View::Default::Text'
274             };
275              
276             sub _initial_aspects {
277             return (
278             'id',
279             'name',
280             'qty_on_hand',
281             'outstanding_orders' => [
282             'id',
283             'status',
284             'customer' => [
285             'id',
286             'name',
287             ]
288             ],
289             );
290             }
291              
292             $v = $o->create_view(perspective => 'outstanding orders', toolkit => 'text');
293             print $v->content;
294              
295             =head1 SEE ALSO
296              
297             UR::Object::View, UR::Object::View::Toolkit::Text, UR::Object
298              
299             =cut
300