import React from 'react'; import inflection from 'inflection'; import getValuesFromRecords from './getValuesFromRecords'; import InferredElement from './InferredElement'; import { isObject, valuesAreArray, valuesAreBoolean, valuesAreDate, valuesAreDateString, valuesAreHtml, valuesAreInteger, valuesAreNumeric, valuesAreObject, valuesAreString, } from './assertions'; import { InferredTypeMap } from './types'; const DefaultComponent = () => ;; const defaultType = { type: DefaultComponent, representation: () => '', }; const defaultTypes = { array: defaultType, boolean: defaultType, date: defaultType, email: defaultType, id: defaultType, number: defaultType, reference: defaultType, referenceArray: defaultType, richText: defaultType, string: defaultType, url: defaultType, }; const hasType = (type, types) => typeof types[type] !== 'undefined'; /** * Guesses an element based on an array of values * * @example * inferElementFromValues( * 'address', * ['2 Baker Street', '1 Downing street'], * { number: NumberField, string: StringField } * ); * // new InferredElement() * * Types are optional: if a type isn't provided, the function falls back * to the neareast type. * * @example * inferElementFromValues( * 'content', * ['

Hello

'], * { string: StringField } // no richText type * ); * // new InferredElement() * * Types can be disabled by passing a falsy value. * * @example * inferElementFromValues( * 'content', * ['

Hello

'], * { string: StringField, richText: false } * ); * // null * * @param {String} name Property name, e.g. 'date_of_birth' * @param {[mixed]} values an array of values from which to determine the type, e.g. [12, 34.4, 43] * @param {Object} types A set of components indexed by type. The string type is the only required one * * @return InferredElement */ const inferElementFromValues = ( name, values = [], types: InferredTypeMap = defaultTypes ) => { if (name === 'id' && hasType('id', types)) { return new InferredElement(types.id, { source: name }); } if (name.substr(name.length - 3) === '_id' && hasType('reference', types)) { const reference = inflection.pluralize(name.substr(0, name.length - 3)); return ( types.reference && new InferredElement( types.reference, { source: name, reference, }, new InferredElement(types.referenceChild) ) ); } if (name.substr(name.length - 2) === 'Id' && hasType('reference', types)) { const reference = inflection.pluralize(name.substr(0, name.length - 2)); return ( types.reference && new InferredElement( types.reference, { source: name, reference, }, new InferredElement(types.referenceChild) ) ); } if ( name.substr(name.length - 4) === '_ids' && hasType('referenceArray', types) ) { const reference = inflection.pluralize(name.substr(0, name.length - 4)); return ( types.referenceArray && new InferredElement( types.referenceArray, { source: name, reference, }, new InferredElement(types.referenceArrayChild) ) ); } if ( name.substr(name.length - 3) === 'Ids' && hasType('referenceArray', types) ) { const reference = inflection.pluralize(name.substr(0, name.length - 3)); return ( types.referenceArray && new InferredElement( types.referenceArray, { source: name, reference, }, new InferredElement(types.referenceArrayChild) ) ); } if (values.length === 0) { // FIXME introspect further using name return new InferredElement(types.string, { source: name }); } if (valuesAreArray(values)) { if (isObject(values[0][0]) && hasType('array', types)) { const leafValues = getValuesFromRecords( values.reduce((acc, vals) => acc.concat(vals), []) ); // FIXME bad visual representation return ( types.array && new InferredElement( types.array, { source: name, }, Object.keys(leafValues).map(leafName => inferElementFromValues( leafName, leafValues[leafName], types ) ) ) ); } // FIXME introspect further return new InferredElement(types.string, { source: name }); } if (valuesAreBoolean(values) && hasType('boolean', types)) { return new InferredElement(types.boolean, { source: name }); } if (valuesAreDate(values) && hasType('date', types)) { return new InferredElement(types.date, { source: name }); } if (valuesAreString(values)) { if (name === 'email' && hasType('email', types)) { return new InferredElement(types.email, { source: name }); } if (name === 'url' && hasType('url', types)) { return new InferredElement(types.url, { source: name }); } if (valuesAreDateString(values) && hasType('date', types)) { return new InferredElement(types.date, { source: name }); } if (valuesAreHtml(values) && hasType('richText', types)) { return new InferredElement(types.richText, { source: name }); } return new InferredElement(types.string, { source: name }); } if ( (valuesAreInteger(values) || valuesAreNumeric(values)) && hasType('number', types) ) { return new InferredElement(types.number, { source: name }); } if (valuesAreObject(values)) { // we need to go deeper // Arbitrarily, choose the first prop of the first object const propName = Object.keys(values[0]).shift(); const leafValues = values.map(v => v[propName]); return inferElementFromValues(`${name}.${propName}`, leafValues, types); } return new InferredElement(types.string, { source: name }); }; export default inferElementFromValues;