# RowBoat API

This is really more of a summary of what you can do with `RowBoat::Base` since you subclass it to do everything :)

## Contents

- [Basic Usage](#basic-usage)
- [`.import`](#import)
- [`initialize`](#initialize)
- [`import`](#import-1)
- [`import_into`](#import_into)
- [`csv_source`](#csv_source)
- [`column_mapping`](#column_mapping)
- [`preprocess_row`](#preprocess_row)
- [`preprocess_rows`](#preprocess_rows)
- [`options`](#options)
- [`handle_failed_row`](#handle_failed_row)
- [`handle_failed_rows`](#handle_failed_rows)
- [`value_converters`](#value_converters)

## Basic Usage

Just subclass `RowBoat::Base` and define the [`import_into`](#import_into) and [`column_mapping`](#column_mapping) methods to get started (They're the only methods that you're required to implement).

```ruby
class ImportProduct < RowBoat::Base
  def import_into
    Product
  end

  def column_mapping
    {
      downcased_csv_column_header: :model_attribute_name,
      another_downcased_csv_column_header: :another_model_attribute_name
    }
  end
end
```

## `.import`

### Description
Imports database records form the given CSV-like object. The CSV-like object can be anything that can be passed to [`SmarterCSV.process`](https://github.com/tilo/smarter_csv#documentation) (string paths to files, files, tempfiles, instances of StringIO, etc).

It returns a hash containing

- `:invalid_records` - an array of all records that failed to import since they were invalid. If you've configured the `:validate` option to be `false` it will be an empty array.
- `:total_inserted` - the total number of records inserted into the database.
- `:inserted_ids` - an array of all of the ids of records inserted into the database.

If you want to pass additional information to help import CSVs, *don't override this method*. It just passes through to [`initialize`](#initialize) so override that :)

### Example

#### Basic Use

```ruby
class ImportProduct < RowBoat::Base
  # required configuration omitted for brevity
end

ImportProduct.import("path/to/my.csv")
```

#### Advanced Use

```ruby
class ImportProduct < RowBoat::Base
  # required configuration omitted for brevity
  def intitialize(csv_source, my_options)
    super(csv_source)
    @my_options = my_options
  end
end

ImportProduct.import("path/to/my.csv", foo: "bar")
```

## `initialize`

### Description

Makes a new instance with the given CSV-like object. See [`.import`](#import) for more details around when and how to override this method.

## `import`

### Description

The instance method that actually parses and imports the CSV. Generally, you wouldn't call this directly and would instead call [`.import`](#import).

## `import_into`

### Description

It is required that you override this method to return whatever ActiveRecord class you want your CSV imported into.

### Example

#### Basic Use

```ruby
class ImportProduct < RowBoat::Base
  # other required configuration omitted for brevity
  def import_into
    Product
  end
end
```

#### Advanced Use

```ruby
class ImportProduct < RowBoat::Base
  # other required configuration omitted for brevity
  def import_into
    if csv_source.is_a?(String) && csv_source.match(/category/i)
      ProductCategory
    else
      Product
    end
  end
end

ImportProduct.import("path/to/category.csv")
ImportProduct.import("path/to/product.csv")
```


## `csv_source`

### Description

Whatever you originally passed in as the CSV source.

### Example

```ruby
class ImportProduct < RowBoat::Base
  # other required configuration omitted for brevity
  def import_into
    # `csv_source` is available in any of our instance methods
    if csv_source.is_a?(String) && csv_source.match(/category/i)
      ProductCategory
    else
      Product
    end
  end
end

ImportProduct.import("path/to/category.csv")
ImportProduct.import("path/to/product.csv")
```

## `column_mapping`

### Description

It is required that you override this method with either a hash that maps columns in your CSV to their preferred names or an array of your preferred column names.

By default when using a hash
- CSV column names are downcased symbols of what they look like in the CSV.
- CSV columns that are not mapped are ignored when processing the CSV.

If you're familiar with [SmarterCSV](https://github.com/tilo/smarter_csv#documentation), this method essentially defines your `:key_mapping` with the `:remove_unmapped_keys` setting set to `true` when provided with a hash. When given an array it is the `:user_provided_headers` option.

You can change these defaults by overriding the [`options`](#options) method.

### Example

```ruby
class ImportProduct < RowBoat::Base
  # other required configuration omitted for brevity
  def column_mapping
    {
      prdct_nm: :name,
      "price/cost_amnt": :price_in_cents
    }
  end
end

# or...

class ImportProduct < RowBoat::Base
  # other required configuration omitted for brevity
  def column_mapping
    [:name, :price_in_cents]
  end
end
```

## `preprocess_row`

### Description

Implement this method if you need to do some work on the row before the record is inserted/updated.

If you return `nil` from this method, the row will be skipped in the import.

You also have access to `row_number` here.

If the work you intend to do with the row only requires changing one attribute, it is recommended that you override [`value_converters`](#value_converters) instead of this.

### Example

```ruby
  class ImportProduct < RowBoat::Base
    # required configuration omitted for brevity
    def preprocess_row(row)
      { position: row_number }.merge(row)
    end
    # or...
    def preprocess_row(row)
      if row[:name] && row[:price]
        row
      else
        nil
      end
    end
  end
```

## `preprocess_rows`

### Description

Override this method if you need to do something with a chunk of rows (the chunk size is determined by the `:chunk_size` option in the [`options`](#options) method).

If you need to filter particular rows, it's better to just implement [`preprocess_row`](#preprocess_row) and return `nil` for the rows you want to ignore.

### Example

```ruby
class ImportProduct < RowBoat::Base
  # required configuration omitted for brevity
  def preprocess_rows(rows)
    if skip_batch?(rows)
      super([])
    else
      super
    end
  end

  def skip_batch?(rows)
    # decide whether or not to skip the batch
  end
end
```

## `options`

### Description

Implement this to configure RowBoat, [SmarterCSV](https://github.com/tilo/smarter_csv), and [activerecord-import](https://github.com/zdennis/activerecord-import).

Except for `:wrap_in_transaction`, all options pass through to SmarterCSV and activerecord-import.

`:wrap_in_transaction` simply tells RowBoat whether or not you want your whole import wrapped in a database transaction.

Whatever you define in this method will be merged into the defaults:

  - `:chunk_size` - `500`
  - `:key_mapping` - `column_mapping`
  - `:recursive` - `true`
  - `:remove_unmapped_keys` - `true`
  - `:validate` - `true`
  - `:value_converters` - `csv_value_converters`
  - `:wrap_in_transaction` - `true`

Don't provide `value_converters` or `key_mapping` options here. Implement the [`value_converters`](#value_converters) and [`column_mapping`](#column_mapping) respectively.

### Example

```ruby
class ImportProduct < RowBoat::Base
  # required configuration omitted for brevity
  def options
    {
      chunk_size: 1000,
      validate: false,
      wrap_in_transaction: false
    }
  end
end
```

## `handle_failed_row`

### Description

Implement this to do some work with a row that has failed to import.

It's important to note that
- This happens after the import has completed.
- The given row is an instance of whatever class was returned by [`import_into`](#import_into).

These records are also available in the return value of [`.import`](#import).

### Example

```ruby
class ImportProduct < RowBoat::Base
  # required configuration omitted for brevity
  def handle_failed_row(row)
    puts row.errors.full_messages.join(", ")
  end
end
```

## `handle_failed_rows`

### Description

Override this method to do some work will all of the rows that failed to import.

### Example

```ruby
class ImportProduct < RowBoat::Base
  # required configuration omitted for brevity
  def handle_failed_rows(rows)
    puts "Failed to import #{rows.size} rows :("
    super
  end
end
```

## `value_converters`

### Description

Implement to specify how to translate values from the CSV into whatever sorts of objects you need.

Simply return a hash that has the mapped column name (ie, what you mapped it to in the [`column_mapping`](#column_mapping) method) as a key pointing to either
- a method name as a symbol
- a proc or lambda
- an object that implements `convert`

Regardless of which one you choose, it takes a value and returns a converted value.

This is essentially a sugared up version of `:value_converters` option in [SmarterCSV](https://github.com/tilo/smarter_csv#documentation).

### Example

```ruby
class ImportProduct < RowBoat::Base
  # required configuration omitted for brevity
  def value_converters
    {
      sell_by: :convert_date,
      name: -> (value) { value.titlelize },
      price: proc { |value| value.to_i },
      description: DescriptionConverter
    }
  end

  def convert_date(value)
    Date.parse(value) rescue nil
  end
end

module DescriptionConverter
  def self.convert(value)
    value.present? ? value : "default description :("
  end
end
```