]> wagnertech.de Git - mfinanz.git/blobdiff - SL/Helper/Csv.pm
Csv Errors sind nun Objekte mit entsprechendem Zugriff.
[mfinanz.git] / SL / Helper / Csv.pm
index d4383779f558f4b20b9cfd11de5f498df4fc047e..945758f5d3c1084df00dc906b539e2a43b018f3f 100644 (file)
@@ -5,14 +5,16 @@ use warnings;
 
 use Carp;
 use IO::File;
-use Text::CSV;
 use Params::Validate qw(:all);
+use Text::CSV;
 use Rose::Object::MakeMethods::Generic scalar => [ qw(
   file encoding sep_char quote_char escape_char header profile class
   numberformat dateformat ignore_unknown_columns _io _csv _objects _parsed
   _data _errors
 ) ];
 
+use SL::Helper::Csv::Dispatcher;
+use SL::Helper::Csv::Error;
 
 # public interface
 
@@ -53,7 +55,7 @@ sub parse {
 
   $self->_open_file;
   return if ! $self->_check_header;
-  return if $self->class && ! $self->_check_header_for_class;
+  return if ! $self->dispatcher->parse_profile;
   return if ! $self->_parse_data;
 
   $self->_parsed(1);
@@ -109,41 +111,6 @@ sub _check_header {
   $self->header($header);
 }
 
-sub _check_header_for_class {
-  my ($self, %params) = @_;
-  my @errors;
-
-  carp 'this should never be called without' unless $self->class;
-
-  if ($self->ignore_unknown_columns) {
-    my @new_header;
-    for my $method (@{ $self->header }) {
-      push @new_header, $self->class->can($self->_real_method($method))
-         ? $method : undef;
-    }
-
-    $self->header(\@new_header);
-
-    return 1;
-  } else {
-    for my $method (@{ $self->header }) {
-      next if ! $method;
-      next if $self->class->can($self->_real_method($method));
-
-      push @errors, [
-        $method,
-        undef,
-        "header field $method is not recognized",
-        undef,
-        0,
-      ];
-    }
-
-    $self->_push_error(@errors);
-    return ! @errors;
-  }
-}
-
 sub _parse_data {
   my ($self, %params) = @_;
   my (@data, @errors);
@@ -153,7 +120,6 @@ sub _parse_data {
   while (1) {
     my $row = $self->_csv->getline($self->_io);
     last if $self->_csv->eof;
-
     if ($row) {
       my %hr;
       @hr{@{ $self->header }} = @$row;
@@ -186,19 +152,26 @@ sub _make_objects {
   local $::myconfig{dateformat}   = $self->dateformat   if $self->dateformat;
 
   for my $line (@{ $self->_data }) {
-    push @objs, $self->class->new(
-      map {
-        $self->_real_method($_) => $line->{$_}
-      } grep { $_ } keys %$line
-    );
+    my $tmp_obj = $self->class->new;
+    $self->dispatcher->dispatch($tmp_obj, $line);
+    push @objs, $tmp_obj;
   }
 
   $self->_objects(\@objs);
 }
 
-sub _real_method {
-  my ($self, $arg) = @_;
-  ($self->profile && $self->profile->{$arg}) || $arg;
+sub dispatcher {
+  my ($self, %params) = @_;
+
+  $self->{_dispatcher} ||= $self->_make_dispatcher;
+}
+
+sub _make_dispatcher {
+  my ($self, %params) = @_;
+
+  die 'need a header to make a dispatcher' unless $self->header;
+
+  return SL::Helper::Csv::Dispatcher->new($self);
 }
 
 sub _guess_encoding {
@@ -208,7 +181,7 @@ sub _guess_encoding {
 
 sub _push_error {
   my ($self, @errors) = @_;
-  my @new_errors = ($self->errors, @errors);
+  my @new_errors = ($self->errors, map { SL::Helper::Csv::Error->new(@$_) } @errors);
   $self->_errors(\@new_errors);
 }
 
@@ -231,15 +204,18 @@ SL::Helper::Csv - take care of csv file uploads
     file        => \$::form->{upload_file},
     encoding    => 'utf-8', # undef means utf8
     sep_char    => ',',     # default ';'
-    quote_char  => ''',     # default '"'
-    header      => [qw(id text sellprice word)] # see later
-    profile    => { sellprice => 'sellprice_as_number' }
+    quote_char  => '\'',    # default '"'
+    escape_char => '"',     # default '"'
+    header      => [qw(id text sellprice word)], # see later
+    profile     => { sellprice => 'sellprice_as_number' },
     class       => 'SL::DB::CsvLine',   # if present, map lines to this
-  )
+  );
 
   my $status  = $csv->parse;
   my $hrefs   = $csv->get_data;
-  my @objects = $scv->get_objects;
+  my @objects = $csv->get_objects;
+
+  my @errors  = $csv->errors;
 
 =head1 DESCRIPTION
 
@@ -249,8 +225,31 @@ Text::CSV offeres already good functions to get lines out of a csv file, but in
 most cases you will want those line to be parsed into hashes or even objects,
 so this model just skips ahead and gives you objects.
 
-Encoding autodetection is not easy, and should not be trusted. Try to avoid it
-if possible.
+Its basic assumptions are:
+
+=over 4
+
+=item You do know what you expect to be in that csv file.
+
+This means first and foremost you have knowledge about encoding, number and
+date format, csv parameters such as quoting and separation characters. You also
+know what content will be in that csv and what L<Rose::DB> is responsible for
+it. You provide valid header columns and their mapping to the objects.
+
+=item You do NOT know if the csv provider yields to your expectations.
+
+Stuff that does not work with what you expect should not crash anything, but
+give you a hint what went wrong. As a result, if you remeber to check for
+errors after each step, you should be fine.
+
+=item Data does not make sense. It's just data.
+
+Almost all data imports have some type of constraints. Some data needs to be
+unique, other data needs to be connected to existing data sets. This will not
+happen here. You will receive a plain mapping of the data into the class tree,
+nothing more.
+
+=back
 
 =head1 METHODS
 
@@ -276,7 +275,7 @@ Returns an arrayref of the raw lines as hashrefs.
 
 =item C<errors>
 
-Return all errors that came up druing parsing. See error handling for detailed
+Return all errors that came up during parsing. See error handling for detailed
 information.
 
 =back
@@ -293,7 +292,7 @@ scalar ref for memory data.
 =item C<encoding>
 
 Encoding of the CSV file. Note that this module does not do any encoding
-guessing.  Know what your data ist. Defaults to utf-8.
+guessing. Know what your data is. Defaults to utf-8.
 
 =item C<sep_char>
 
@@ -317,6 +316,22 @@ May be used to map header fields to custom accessors. Example:
 In this case C<listprice_as_number> will be used to read in values from the
 C<listprice> column.
 
+In case of a One-To-One relationsship these can also be set over
+relationsships by sparating the steps with a dot (C<.>). This will work:
+
+  { customer => 'customer.name' }
+
+And will result in something like this:
+
+  $obj->customer($obj->meta->relationship('customer')->class->new);
+  $obj->customer->name($csv_line->{customer})
+
+But beware, this will not try to look up anything in the database. You will
+simply receive objects that represent what the profile defined. If some of
+these information are unique, and should be connected to preexisting data, you
+will have to do that for yourself. Since you provided the profile, it is
+assumed you know what to do in this case.
+
 =item C<class>
 
 If present, the line will be handed to the new sub of this class,
@@ -332,16 +347,13 @@ but deactivated by default.
 =head1 ERROR HANDLING
 
 After parsing a file all errors will be accumulated into C<errors>.
+Each entry is an object with the following attributes:
 
-Each entry is an arrayref with the following structure:
-
- [
- 0  offending raw input,
- 1  Text::CSV error code if T:C error, 0 else,
- 2  error diagnostics,
- 3  position in line,
- 4  estimated line in file,
- ]
+ raw_input:  offending raw input,
+ code:   Text::CSV error code if Text:CSV signalled an error, 0 else,
+ diag:   error diagnostics,
+ line:   position in line,
+ col:    estimated line in file,
 
 Note that the last entry can be off, but will give an estimate.