File Coverage

blib/lib/Text/KDL/XS/Emitter.pm
Criterion Covered Total %
statement 112 119 94.1
branch 53 72 73.6
condition 19 33 57.5
subroutine 15 15 100.0
pod n/a
total 199 239 83.2


line stmt bran cond sub pod time code
1             package Text::KDL::XS::Emitter;
2              
3 3     3   3523 use strict;
  3         5  
  3         120  
4 3     3   12 use warnings;
  3         5  
  3         149  
5              
6 3     3   11 use Carp ();
  3         4  
  3         40  
7 3     3   14 use Scalar::Util ();
  3         9  
  3         54  
8 3     3   11 use B ();
  3         4  
  3         3985  
9              
10             # Tree-emit entry point:
11             # $string = Text::KDL::XS::Emitter->_emit_tree($tree, %opts)
12             #
13             # Two modes, dispatched on input type:
14             #
15             # * Tree mode (explicit, full fidelity):
16             # - Text::KDL::XS::Document
17             # - Text::KDL::XS::Node
18             # - arrayref whose elements are all Text::KDL::XS::Node objects
19             #
20             # * Data mode (auto-convert plain Perl data):
21             # - any other HASH ref or ARRAY ref
22             #
23             # Data-mode mapping is documented in L.
24             sub _emit_tree {
25 13     13   32 my ($class, $tree, %opts) = @_;
26              
27 13   100     66 my $version = $opts{version} // 'detect';
28 13 50       51 my $version_int
    100          
    50          
29             = $version eq '1' ? 1
30             : $version eq '2' ? 2
31             : $version eq 'detect' ? 0
32             : Carp::croak("emit_kdl: unknown version '$version'");
33              
34 13 50       31 my $indent = defined $opts{indent} ? $opts{indent} : -1;
35 13 50       24 my $escape_mode = defined $opts{escape_mode} ? $opts{escape_mode} : -1;
36 13 50       27 my $identifier_mode = defined $opts{identifier_mode} ? $opts{identifier_mode} : -1;
37              
38 13         119 my $emitter = $class->_new($version_int, $indent, $escape_mode, $identifier_mode);
39              
40 13   100     49 my $blessed = Scalar::Util::blessed($tree) // '';
41 13 100 66     43 if ($blessed eq 'Text::KDL::XS::Document') {
    50          
    50          
    50          
42 2         24 _emit_node_recursive($emitter, $_) for @{ $tree->nodes };
  2         8  
43             }
44             elsif ($blessed eq 'Text::KDL::XS::Node') {
45 0         0 _emit_node_recursive($emitter, $tree);
46             }
47             elsif (_is_node_array($tree)) {
48 0         0 _emit_node_recursive($emitter, $_) for @$tree;
49             }
50             elsif (ref($tree) eq 'HASH' || ref($tree) eq 'ARRAY') {
51 11         42 _emit_data($emitter, $tree);
52             }
53             else {
54 0         0 Carp::croak("emit_kdl: expected Document, Node, ARRAY ref, or HASH ref");
55             }
56              
57 12         36 $emitter->_emit_end;
58 12         91 return $emitter->_get_buffer;
59             }
60              
61             sub _is_node_array {
62 11     11   20 my ($x) = @_;
63 11 100       54 return 0 unless ref($x) eq 'ARRAY';
64 2         6 for my $el (@$x) {
65 2         5 my $b = Scalar::Util::blessed($el);
66 2 50 33     19 return 0 unless $b && $el->isa('Text::KDL::XS::Node');
67             }
68 0 0       0 return @$x ? 1 : 0; # empty array goes to data mode (trivially empty)
69             }
70              
71             # ---------------------------------------------------------------------------
72             # Tree mode (explicit)
73             # ---------------------------------------------------------------------------
74              
75             sub _emit_node_recursive {
76 5     5   9 my ($emitter, $node) = @_;
77              
78 5 50 0     33 Carp::croak("emit_kdl: tree mode expects Text::KDL::XS::Node, got "
      33        
79             . (ref($node) || 'scalar'))
80             unless Scalar::Util::blessed($node) && $node->isa('Text::KDL::XS::Node');
81              
82 5         12 $emitter->_emit_node($node->name, $node->type_annotation);
83              
84 5         9 $emitter->_emit_arg(_value_to_payload($_)) for @{ $node->args };
  5         9  
85              
86 5         8 for my $pair (@{ $node->props }) {
  5         11  
87 1         2 my ($key, $value) = @$pair;
88 1         1 $emitter->_emit_property($key, _value_to_payload($value));
89             }
90              
91 5         11 my $children = $node->children;
92 5 100       12 if (@$children) {
93 1         3 $emitter->_start_children;
94 1         11 _emit_node_recursive($emitter, $_) for @$children;
95 1         2 $emitter->_finish_children;
96             }
97             }
98              
99             sub _value_to_payload {
100 34     34   57 my ($v) = @_;
101              
102 34 100 100     103 if (Scalar::Util::blessed($v) && $v->isa('Text::KDL::XS::Value')) {
103             return {
104             type => $v->{type},
105             kind => $v->{kind},
106             value => $v->{value},
107             type_annotation => $v->{type_annotation},
108 6         43 };
109             }
110 28         49 return _coerce_scalar_to_payload($v);
111             }
112              
113             # ---------------------------------------------------------------------------
114             # Data mode (auto-convert plain Perl data)
115             # ---------------------------------------------------------------------------
116              
117             # Convention: a top-level arrayref becomes a series of anonymous nodes named
118             # "-" (the same convention used by JSON-in-KDL). Top-level hashrefs become
119             # a series of nodes, one per key, in sorted-key order for deterministic
120             # output.
121             sub _emit_data {
122 11     11   22 my ($emitter, $data) = @_;
123              
124 11 100       27 if (ref($data) eq 'HASH') {
125 9         37 for my $key (sort keys %$data) {
126 15         35 _emit_data_pair($emitter, $key, $data->{$key});
127             }
128 8         15 return;
129             }
130              
131 2 50       6 if (ref($data) eq 'ARRAY') {
132 2         6 for my $item (@$data) {
133 5         13 _emit_data_pair($emitter, '-', $item);
134             }
135 2         5 return;
136             }
137              
138 0         0 Carp::croak("emit_kdl: top-level data must be HASH or ARRAY ref");
139             }
140              
141             # Emit one (name, value) pair according to the documented mapping:
142             #
143             # scalar / undef / bool -> `name `
144             # [] -> bare `name`
145             # [ scalars... ] -> `name ...`
146             # { ... } -> `name { children }`
147             # [ $non_scalar, ... ] -> repeated sibling `name`s (one per element)
148             #
149             # Mixed arrays (some scalars, some refs) repeat the sibling form for each
150             # element, scalars included.
151             sub _emit_data_pair {
152 37     37   98 my ($emitter, $name, $value) = @_;
153              
154 37 100 100     121 if (!ref($value) || _is_bool_object($value) || _is_value_object($value)) {
      66        
155 25         103 $emitter->_emit_node($name, undef);
156 25         49 $emitter->_emit_arg(_value_to_payload($value));
157 25         99 return;
158             }
159              
160 12 100       35 if (ref($value) eq 'HASH') {
161 7         39 $emitter->_emit_node($name, undef);
162 7 50       18 if (%$value) {
163 7         20 $emitter->_start_children;
164 7         28 for my $k (sort keys %$value) {
165 13         32 _emit_data_pair($emitter, $k, $value->{$k});
166             }
167 7         20 $emitter->_finish_children;
168             }
169 7         18 return;
170             }
171              
172 5 100       18 if (ref($value) eq 'ARRAY') {
173 4 100       43 if (!@$value) {
174 1         15 $emitter->_emit_node($name, undef);
175 1         3 return;
176             }
177              
178 3         7 my $all_scalar = !grep { _is_complex($_) } @$value;
  7         14  
179 3 100       9 if ($all_scalar) {
180 1         6 $emitter->_emit_node($name, undef);
181 1         3 $emitter->_emit_arg(_value_to_payload($_)) for @$value;
182 1         2 return;
183             }
184              
185             # Mixed/complex: repeat the sibling for each element.
186 2         9 _emit_data_pair($emitter, $name, $_) for @$value;
187 2         5 return;
188             }
189              
190 1         231 Carp::croak("emit_kdl: cannot serialize " . ref($value) . " ref");
191             }
192              
193             sub _is_complex {
194 7     7   12 my ($v) = @_;
195 7 100       15 return 0 unless ref $v;
196 4 50       9 return 0 if _is_bool_object($v);
197 4 50       9 return 0 if _is_value_object($v);
198 4         12 return 1;
199             }
200              
201             sub _is_bool_object {
202 45     45   74 my ($v) = @_;
203 45         65 my $b = Scalar::Util::blessed($v);
204 45 100       139 return 0 unless $b;
205 4   0     22 return $b eq 'JSON::PP::Boolean'
206             || $b eq 'Types::Serialiser::Boolean'
207             || $b eq 'JSON::Boolean'
208             || $b eq 'boolean'
209             || $b eq 'Mojo::JSON::_Bool';
210             }
211              
212             sub _is_value_object {
213 16     16   29 my ($v) = @_;
214 16         28 my $b = Scalar::Util::blessed($v);
215 16   33     57 return $b && $b eq 'Text::KDL::XS::Value';
216             }
217              
218             # Coerce a plain Perl scalar (or bool object) into the C-friendly payload
219             # hash that the XS layer consumes.
220             #
221             # undef -> KDL null
222             # JSON::PP::true / Types::Serialiser::* -> KDL bool
223             # integer-flagged SV -> KDL number (integer)
224             # float-flagged SV -> KDL number (float)
225             # any other scalar -> KDL string
226             #
227             # Strings such as "true"/"false" are NOT heuristically promoted to booleans;
228             # pass an explicit JSON::PP::true / JSON::PP::false if you mean a bool.
229             sub _coerce_scalar_to_payload {
230 28     28   44 my ($v) = @_;
231              
232 28 100       85 return { type => 'null', value => undef } unless defined $v;
233              
234 27 100       45 if (_is_bool_object($v)) {
235 2 100       37 return { type => 'bool', value => ($v ? 1 : 0) };
236             }
237              
238 25 50       48 if (Scalar::Util::blessed($v)) {
239             # Stringifiable objects (Math::BigInt, URIs, etc.) - preserve as string.
240 0         0 return { type => 'string', value => "$v" };
241             }
242              
243 25 50       52 Carp::croak("emit_kdl: refs cannot appear as a single scalar value here")
244             if ref $v;
245              
246 25         109 my $flags = B::svref_2object(\$v)->FLAGS;
247 25   66     116 my $is_string_only = ($flags & B::SVf_POK()) && !($flags & (B::SVf_IOK() | B::SVf_NOK()));
248              
249 25 100       155 return { type => 'string', value => "$v" } if $is_string_only;
250              
251 5 100       14 if ($flags & B::SVf_IOK()) {
252 4         29 return { type => 'number', kind => 'integer', value => 0 + $v };
253             }
254 1 50       5 if ($flags & B::SVf_NOK()) {
255 1         16 return { type => 'number', kind => 'float', value => 0 + $v };
256             }
257              
258 0           return { type => 'string', value => "$v" };
259             }
260              
261             1;
262              
263             __END__