Projekt

Allgemein

Profil

« Zurück | Weiter » 

Revision 9414d575

Von Sven Schöling vor mehr als 12 Jahren hinzugefügt

  • ID 9414d575c80bf2e7278e1bac7d0975809e79abe3
  • Vorgänger 2d16e7a2
  • Nachfolger ba0fb69c

Request: flatten und unflatten implentiert und getestet.

Die Tests stammen zur Hälfte aus Hash::Flatten und haben einige Fehler aufgedeckt.
Damit kann im nächsten Schritt SL::Controller::Base::url_for tiefe
Datenstrukturen serialisieren.

Unterschiede anzeigen:

SL/Request.pm
6 6
use SL::MoreCommon qw(uri_encode uri_decode);
7 7
use List::Util qw(first max min sum);
8 8
use List::MoreUtils qw(all any apply);
9
use Exporter qw(import);
9 10

  
10
sub _store_value {
11
  $::lxdebug->enter_sub(2);
11
our @EXPORT_OK = qw(flatten unflatten read_cgi_input);
12 12

  
13
sub _store_value {
13 14
  my ($target, $key, $value) = @_;
14
  my @tokens = split /((?:\[\+?\])?(?:\.|$))/, $key;
15
  my @tokens = split /((?:\[\+?\])?(?:\.)|(?:\[\+?\]))/, $key;
15 16
  my $curr;
16 17

  
17 18
  if (scalar @tokens) {
......
22 23
    my $sep = shift @tokens;
23 24
    my $key = shift @tokens;
24 25

  
25
    $curr = \ $$curr->[++$#$$curr], next if $sep eq '[]';
26
    $curr = \ $$curr->[$#$$curr], next   if $sep eq '[]' && @tokens;
27
    $curr = \ $$curr->[++$#$$curr], next if $sep eq '[]' && !@tokens;
28
    $curr = \ $$curr->[++$#$$curr], next if $sep eq '[+]';
26 29
    $curr = \ $$curr->[max 0, $#$$curr]  if $sep eq '[].';
27 30
    $curr = \ $$curr->[++$#$$curr]       if $sep eq '[+].';
28 31
    $curr = \ $$curr->{$key}
......
30 33

  
31 34
  $$curr = $value;
32 35

  
33
  $::lxdebug->leave_sub(2);
34

  
35 36
  return $curr;
36 37
}
37 38

  
......
249 250
  return $target;
250 251
}
251 252

  
253
sub flatten {
254
  my ($source, $target, $prefix, $in_array) = @_;
255
  $target ||= [];
256

  
257
  # there are two edge cases that need attention. first: more than one hash
258
  # inside an array.  only the first of each nested can have a [+].  second: if
259
  # an array contains mixed values _store_value will rely on autovivification.
260
  # so any type change must have a [+]
261
  # this closure decides one recursion step AFTER an array has been found if a
262
  # [+] needs to be generated
263
  my $arr_prefix = sub {
264
    return $_[0] ? '[+]' : '[]' if $in_array;
265
    return '';
266
  };
267

  
268
  for (ref $source) {
269
    /^HASH$/ && do {
270
      my $first = 1;
271
      for my $key (keys %$source) {
272
        flatten($source->{$key} => $target, (defined $prefix ? $prefix . $arr_prefix->($first) . '.' : '') . $key);
273
        $first = 0;
274
      };
275
      next;
276
    };
277
    /^ARRAY$/ && do {
278
      for my $i (0 .. $#$source) {
279
        flatten($source->[$i] => $target, $prefix . $arr_prefix->($i == 0), '1');
280
      }
281
      next;
282
    };
283
    !$_ && do {
284
      die "can't flatten a pure scalar" unless defined $prefix;
285
      push @$target, [ $prefix . $arr_prefix->(0) => $source ];
286
      next;
287
    };
288
    die "unrecognized reference of a data structure $_. cannot serialize refs, globs and code yet. to serialize Form please use the method there";
289
  }
290

  
291
  return $target;
292
}
293

  
294

  
295
sub unflatten {
296
  my ($data, $target) = @_;
297
  $target ||= {};
298

  
299
  for my $pair (@$data) {
300
    _store_value($target, @$pair) if defined $pair->[0];
301
  }
302

  
303
  return $target;
304
}
305

  
252 306
1;
253 307

  
254 308
__END__
255 309

  
256 310
=head1 NAME
257 311

  
258
SL::Form.pm - main data object.
312
SL::Request.pm - request parsing and data serialization
259 313

  
260 314
=head1 SYNOPSIS
261 315

  
262
This module handles unpacking of cgi parameters. usually you donÄt want to call
263
anything in here directly,
316
This module handles unpacking of cgi parameters. usually you don't want to call
317
anything in here directly.
318

  
319
  use SL::Request qw(read_cgi_input);
320

  
321
  # read cgi input depending on request type, unflatten and recode
322
  read_cgi_input($target_hash_ref);
323

  
324
  # $hashref and $new_hashref should be identical
325
  my $new_arrayref = flatten($hashref);
326
  my $new_hashref  = unflatten($new_arrayref);
327

  
328

  
329
=head1 DESCRIPTION
330

  
331
This module handles flattening and unflattening of data for request
332
roundtrip purposes. Lx-Office uses the format as described below:
333

  
334
=over 4
335

  
336
=item Hashes
337

  
338
Hash entries will be connected with a dot (C<.>). A simple hash like this
339

  
340
  order => {
341
    item     => 2,
342
    customer => 5
343
  }
344

  
345
will be serialized to
346

  
347
  [ order.item     => 2 ],
348
  [ order.customer => 5 ],
349

  
350
=item Arrays
351

  
352
Arrays will by trailing empty brackets (C<[]>). An hash like this
353

  
354
  selected_id => [ 2, 6, 8, 9 ]
355

  
356
will be flattened to
357

  
358
  [ selected_id[] => 2 ],
359
  [ selected_id[] => 6 ],
360
  [ selected_id[] => 8 ],
361
  [ selected_id[] => 9 ],
362

  
363
Since this will produce identical keys, the resulting flattened list can not be
364
used as a hash. It is however very easy to use this in a template to generate
365
input:
366

  
367
  [% FOREACH id = selected_ids %]
368
    <input type="hidden" name="selected_id[]" value="[% id | html %]">
369
  [% END %]
370

  
371
=item Nested structures
372

  
373
A special version of this are nested hashs in an array, which is very common.
374
The combined operator (C<[].>) will be used. As a special case, every time a new
375
array slice is started, the special convention (C<[+].>) will be used. Again this
376
is because it's easy to write a template with it.
377

  
378
So this
379

  
380
  order => {
381
    orderitems => [
382
      {
383
        id   => 1,
384
        part => 15
385
      },
386
      {
387
        id   => 2,
388
        part => 7
389
      },
390
    ]
391
  }
392

  
393
will be
394

  
395
  [ order.orderitems[+].id  => 1  ],
396
  [ order.orderitems[].part => 15 ],
397
  [ order.orderitems[+].id  => 2  ],
398
  [ order.orderitems[].part => 7  ],
399

  
400
=item Limitations
401

  
402
  The format currently does have certain limitations when compared to other
403
  serialization formats.
404

  
405
=over 4
406

  
407
=item Order
408

  
409
The order of serialized values matters to reconstruct arrays properly. This
410
should rarely be a problem if you just flatten and dump into a url or a field
411
of hiddens.
412

  
413
=item Empty Keys
414

  
415
The current implementation of flatten does produce correct serialization of
416
empty keys, but unflatten is unable to resolve these. Do no use C<''> or
417
C<undef> as keys. C<0> is fine.
418

  
419
=item Key Escaping
420

  
421
You cannot use the tokens C<[]>, C<[+]> and C<.> in keys. No way around it.
422

  
423
=item Sparse Arrays
424

  
425
It is not possible to serialize somehing like
426

  
427
  sparse_array => do { my $sa = []; $sa[100] = 1; $sa },
428

  
429
This is a feature, as perl doesn't do well with very large arrays.
430

  
431
=item Recursion
432

  
433
There is currently no support nor prevention for flattening a circular structure.
434

  
435
=item Custom Delimiter
436

  
437
No support for other delimiters, sorry.
438

  
439
=item Other References
440

  
441
No support for globs, scalar refs, code refs, filehandles and the like. These will die.
442

  
443
=back
444

  
445
=back
446

  
447
=head1 FUNCTIONS
448

  
449
=over 4
450

  
451
=item C<flatten HASHREF [ ARRAYREF ]>
452

  
453
This function will flatten the provided hash ref into the provided array ref.
454
The array ref may be non empty, but will be changed in this case.
455

  
456
Return value is the flattened array ref.
457

  
458
=item C<unflatten ARRAYREF [ HASHREF ]>
459

  
460
This function will parse the array ref, and will store the contents into the hash ref. The hash ref may be non empty, in this case any new keys will override the old ones only on leafs with same type. Type changes on a node will die.
264 461

  
265
  SL::Request::read_cgi_input($target_hash_ref);
462
=back
266 463

  
267 464
=head1 SPECIAL FUNCTIONS
268 465

  
......
271 468
parses a complex var name, and stores it in the form.
272 469

  
273 470
syntax:
274
  $form->_store_value($key, $value);
471
  _store_value($target, $key, $value);
275 472

  
276 473
keys must start with a string, and can contain various tokens.
277 474
supported key structures are:
t/request/flatten.t
1
use Test::More;
2
use Test::Deep;
3
use Data::Dumper;
4

  
5
use_ok 'SL::Request', qw(flatten unflatten);
6

  
7
use constant DEBUG => 0;
8

  
9
sub f ($$$) {
10
  my $flat = flatten($_[0]);
11
  print Dumper($flat) if DEBUG;
12

  
13
  my $unflat = unflatten($flat);
14
  print Dumper($unflat) if DEBUG;
15

  
16
  cmp_deeply($flat, $_[1], $_[2]);
17
  cmp_deeply($unflat, $_[0], $_[2]);
18
}
19

  
20
f {
21
  test => 1,
22
  whut => 2
23
},
24
[
25
  [ test => 1 ],
26
  [ whut => 2 ],
27
], 'simple case';
28

  
29
f { a => { b => 2 } },
30
[
31
 [ 'a.b' => 2 ]
32
], 'simple hash nesting';
33

  
34
f { a => [ 2,  4 ] },
35
[
36
 [  'a[]' => 2 ],
37
 [  'a[]' => 4 ],
38
], 'simple array';
39

  
40
f { a => [ { c => 1, d => 2 }, { c => 3, d => 4 }, ] },
41
[
42
  [ 'a[+].c', 1 ],
43
  [ 'a[].d', 2 ],
44
  [ 'a[+].c', 3 ],
45
  [ 'a[].d', 4  ],
46
], 'array of hashes';
47

  
48
# tests from Hash::Flatten below
49
f {
50
  'x' => 1,
51
  'y' => {
52
    'a' => 2,
53
    'b' => {
54
      'p' => 3,
55
      'q' => 4
56
    },
57
  }
58
}, bag(
59
 [ 'x'     => 1, ],
60
 [ 'y.a'   => 2, ],
61
 [ 'y.b.p' => 3, ],
62
 [ 'y.b.q' => 4  ],
63
), 'Hash::Flatten 1';
64

  
65

  
66
f {
67
  'x' => 1,
68
  '0' => {
69
    '1' => 2,
70
  },
71
  'a' => [1,2,3],
72
},
73
bag (
74
 ['x'    => 1, ],
75
 ['0.1'  => 2, ],
76
 ['a[]'  => 1, ],
77
 ['a[]'  => 2, ],
78
 ['a[]'  => 3, ],
79
), 'Hash::Flatten 2 - weird keys and values';
80

  
81

  
82
f {
83
  'x' => 1,
84
  'ay' => {
85
    'a' => 2,
86
    'b' => {
87
      'p' => 3,
88
      'q' => 4
89
    },
90
  },
91
  'y' => [
92
    'a', 2,
93
    {
94
      'baz' => 'bum',
95
    },
96
  ]
97
},
98
bag(
99
  [ 'ay.b.p'  => 3,       ],
100
  [ 'ay.b.q'  => 4,       ],
101
  [ 'ay.a'    => 2,       ],
102
  [ 'x'       => 1,       ],
103
  [ 'y[]'     => 'a',    ],
104
  [ 'y[]'     => 2        ],
105
  [ 'y[+].baz' => 'bum',  ],
106
), 'Hash::Flatten 3 - mixed';
107

  
108
f {
109
  'x' => 1,
110
  'y' => [
111
    [
112
      'a', 'fool', 'is',
113
    ],
114
    [
115
      'easily', [ 'parted', 'from' ], 'his'
116
    ],
117
    'money',
118
  ]
119
},
120
bag(
121
 [ 'x'        => 1,        ],
122
 [ 'y[][]'    => 'his',    ],
123
 [ 'y[][+][]' => 'parted', ],
124
 [ 'y[][][]'  => 'from',   ],
125
 [ 'y[+][]'   => 'a',      ],
126
 [ 'y[+][]'   => 'easily', ],
127
 [ 'y[][]'    => 'fool',   ],
128
 [ 'y[][]'    => 'is'      ],
129
 [ 'y[]'      => 'money',  ],
130
), 'Hash::Flatten 4 - array nesting';
131

  
132
f {
133
  'x' => 1,
134
  'ay' => {
135
    'a' => 2,
136
    'b' => {
137
      'p' => 3,
138
      'q' => 4
139
    },
140
  },
141
  's' => 'hey',
142
  'y' => [
143
    'a', 2, {
144
      'baz' => 'bum',
145
    },
146
  ]
147
},
148
bag(
149
  [ 'x'        => 1,     ],
150
  [ 's'        => 'hey', ],
151
  [ 'ay.a'     => 2,     ],
152
  [ 'y[+].baz' => 'bum', ],
153
  [ 'ay.b.p'   => 3,     ],
154
  [ 'y[]'      => 'a',   ],
155
  [ 'ay.b.q'   => 4,     ],
156
  [ 'y[]'      => 2      ],
157
), 'Hash::Flatten 5 - deep mix';
158

  
159
done_testing();

Auch abrufbar als: Unified diff