Skip to content

Add type casting to CSV fields #82

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
35 changes: 32 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ Primary wrapper component.
},
age: {
required: true,
label: 'Age'
label: 'Age',
type: Number
}
}"
>
Expand All @@ -101,16 +102,44 @@ Primary wrapper component.

| Prop | Default | Description |
| ------ | ------- | ----------- |
| fields | null | (required) The field names used to map the CSV. |
| fields | null | (required) The fields used to map the CSV - see below. |
| text | see below | (optional) Override the default text used in the component. |
| modelValue | N/A | (optional) Binds to the mapped CSV object. |

#### Default text
##### Fields Prop

The fields prop may be a simple array (e.g. `['name', 'age']`) or an object with the following properties:

| Prop | Default | Description |
| ------ | ------- | ----------- |
| required | true | (required) The field names used to map the CSV. |
| label | N/A | (required) Override the default text used in the component. |
| type | String | (optional) A primitive object used to cast the field value - see below |

The type property supports casting with primitive objects, including String, Number, BigInt, and Boolean.

- Number fields - should contain numeric values
- BigInt fields - should contain valid integers or strings (incl. hex, oct, or binary)
- Boolean fields - should include true, false, yes, no, on, off, 1, 0, null or an empty string

Field values that are incompatible with the specified type will be returned as strings and will generate an error.

A closure, may also be assigned to the type property for more complex casting:
```
discount: {
required: true,
label: 'Discount %',
type: (v) => Number((v / 100)) }
}
```

##### Default Text Prop

```json
{
errors: {
fileRequired: 'A file is required',
invalidFieldType: "Invalid data type in row ${row} column ${col}. ",
invalidMimeType: "Invalid file type"
},
toggleHeaders: 'File has headers',
Expand Down
49 changes: 46 additions & 3 deletions src/components/VueCsvImport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
import map from 'lodash/map';
import set from 'lodash/set';
import merge from 'lodash/merge';
import pickBy from 'lodash/pickBy';

const defaultLanguage = {
errors: {
fileRequired: 'A file is required',
invalidFieldType: 'Invalid data type in row ${row} column ${col}. ',
invalidMimeType: "Invalid file type"
},
toggleHeaders: 'File has headers',
Expand All @@ -46,6 +48,7 @@
key: key,
label: get(val, 'label', val),
required: get(val, 'required', true),
type: get(val, 'type', String),
};
});
}
Expand Down Expand Up @@ -84,10 +87,28 @@
const buildMappedCsv = function () {
let newCsv = VueCsvImportData.fileHasHeaders ? VueCsvImportData.rawCsv : drop(VueCsvImportData.rawCsv);

VueCsvImportData.value = map(newCsv, (row) => {
VueCsvImportData.errors = [];

// We only want to include mapped fields because deselected fields remain in the map object
// so we'll exclude anything in VueCsvImportData.map that isn't mapped to a column number
const currentlyMapped = pickBy(VueCsvImportData.map, (v) => Number.isInteger(v));

VueCsvImportData.value = map(newCsv, (row, index) => {
let newRow = {};
forEach(VueCsvImportData.map, (column, field) => {
set(newRow, field, get(row, column));

forEach(currentlyMapped, (column, field) => {
let fieldVal = get(row, column);
try {
fieldVal = typeCast(field, fieldVal);
} catch(err) {
VueCsvImportData.errors.push(
defaultLanguage.errors.invalidFieldType
.replace(/\${row}/g, index + (VueCsvImportData.fileHasHeaders ? 1 : 2))
.replace(/\${col}/g, column + 1)
);
} finally {
set(newRow, field, fieldVal);
}
});

return newRow;
Expand All @@ -96,6 +117,28 @@
emit('update:modelValue', VueCsvImportData.value);
};

const typeMap = VueCsvImportData.fields.reduce((a, f) => set(a, f.key, f.type ?? String), {});

const typeCast = function(field, fieldVal) {
let castVal = typeMap[field](fieldVal);

// Handle Booleans
if (typeMap[field] === Boolean) {
switch (fieldVal.toLowerCase().trim()) {
case 'false': case 'no': case 'off': case '0': case 'null': case '': return false;
case 'true': case 'yes': case 'on': case '1': return true;
default: throw 'Not a boolean!';
}
}

// Catch non-numeric Numbers
if (Object.is(NaN, castVal)) {
throw 'Not a number!';
}

return castVal;
};

provide('VueCsvImportData', VueCsvImportData);
provide('buildMappedCsv', buildMappedCsv);

Expand Down