Skip to content

Commit

Permalink
Merge pull request #5485 from marmelab/expose-inference-data
Browse files Browse the repository at this point in the history
Add ability to infer field type from data
  • Loading branch information
djhi authored Nov 4, 2020
2 parents f0013de + b9196a2 commit 19b48ad
Show file tree
Hide file tree
Showing 3 changed files with 337 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/ra-core/src/inference/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import getElementsFromRecords from './getElementsFromRecords';
import InferredElement from './InferredElement';

export * from './inferTypeFromValues';

export { getElementsFromRecords, InferredElement };
169 changes: 169 additions & 0 deletions packages/ra-core/src/inference/inferTypeFromValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import inflection from 'inflection';

import getValuesFromRecords from './getValuesFromRecords';

import {
isObject,
valuesAreArray,
valuesAreBoolean,
valuesAreDate,
valuesAreDateString,
valuesAreHtml,
valuesAreInteger,
valuesAreNumeric,
valuesAreObject,
valuesAreString,
} from './assertions';

const types = [
'array',
'boolean',
'date',
'email',
'id',
'number',
'reference',
'referenceChild',
'referenceArray',
'referenceArrayChild',
'richText',
'string',
'url',
] as const;

export type PossibleInferredElementTypes = typeof types[number];

export interface InferredElementDescription {
type: PossibleInferredElementTypes;
props?: any;
children?: InferredElementDescription | InferredElementDescription[];
}

/**
* Guesses an element type based on an array of values
*
* @example
* inferElementFromValues(
* 'address',
* ['2 Baker Street', '1 Downing street'],
* );
* // { type: 'string', props: { source: 'address' } }
*
* @param {string} name Property name, e.g. 'date_of_birth'
* @param {any[]} values an array of values from which to determine the type, e.g. [12, 34.4, 43]
*/
export const inferTypeFromValues = (
name,
values = []
): InferredElementDescription => {
if (name === 'id') {
return { type: 'id', props: { source: name } };
}
if (name.substr(name.length - 3) === '_id') {
return {
type: 'reference',
props: {
source: name,
reference: inflection.pluralize(
name.substr(0, name.length - 3)
),
},
children: { type: 'referenceChild' },
};
}
if (name.substr(name.length - 2) === 'Id') {
return {
type: 'reference',
props: {
source: name,
reference: inflection.pluralize(
name.substr(0, name.length - 2)
),
},
children: { type: 'referenceChild' },
};
}
if (name.substr(name.length - 4) === '_ids') {
return {
type: 'referenceArray',
props: {
source: name,
reference: inflection.pluralize(
name.substr(0, name.length - 4)
),
},
children: { type: 'referenceArrayChild' },
};
}
if (name.substr(name.length - 3) === 'Ids') {
return {
type: 'referenceArray',
props: {
source: name,
reference: inflection.pluralize(
name.substr(0, name.length - 3)
),
},
children: { type: 'referenceArrayChild' },
};
}
if (values.length === 0) {
if (name === 'email') {
return { type: 'email', props: { source: name } };
}
if (name === 'url') {
return { type: 'url', props: { source: name } };
}
// FIXME introspect further using name
return { type: 'string', props: { source: name } };
}
if (valuesAreArray(values)) {
if (isObject(values[0][0])) {
const leafValues = getValuesFromRecords(
values.reduce((acc, vals) => acc.concat(vals), [])
);
// FIXME bad visual representation
return {
type: 'array',
props: { source: name },
children: Object.keys(leafValues).map(leafName =>
inferTypeFromValues(leafName, leafValues[leafName])
),
};
}
// FIXME introspect further
return { type: 'string', props: { source: name } };
}
if (valuesAreBoolean(values)) {
return { type: 'boolean', props: { source: name } };
}
if (valuesAreDate(values)) {
return { type: 'date', props: { source: name } };
}
if (valuesAreString(values)) {
if (name === 'email') {
return { type: 'email', props: { source: name } };
}
if (name === 'url') {
return { type: 'url', props: { source: name } };
}
if (valuesAreDateString(values)) {
return { type: 'date', props: { source: name } };
}
if (valuesAreHtml(values)) {
return { type: 'richText', props: { source: name } };
}
return { type: 'string', props: { source: name } };
}
if (valuesAreInteger(values) || valuesAreNumeric(values)) {
return { type: 'number', props: { 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 inferTypeFromValues(`${name}.${propName}`, leafValues);
}
return { type: 'string', props: { source: name } };
};
166 changes: 166 additions & 0 deletions packages/ra-core/src/inference/inferTypesFromValues.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import expect from 'expect';
import { inferTypeFromValues } from './inferTypeFromValues';

describe('inferTypeFromValues', () => {
it('should return an InferredElement', () => {
expect(inferTypeFromValues('id', ['foo'])).toEqual({
type: 'id',
props: { source: 'id' },
});
});
it('should return an id field for field named id', () => {
expect(inferTypeFromValues('id', ['foo', 'bar'])).toEqual({
type: 'id',
props: { source: 'id' },
});
});
it('should return a reference field for field named *_id', () => {
expect(inferTypeFromValues('foo_id', ['foo', 'bar'])).toEqual({
type: 'reference',
props: { source: 'foo_id', reference: 'foos' },
children: { type: 'referenceChild' },
});
});
it('should return a reference field for field named *Id', () => {
expect(inferTypeFromValues('fooId', ['foo', 'bar'])).toEqual({
type: 'reference',
props: { source: 'fooId', reference: 'foos' },
children: { type: 'referenceChild' },
});
});
it('should return a reference array field for field named *_ids', () => {
expect(inferTypeFromValues('foo_ids', ['foo', 'bar'])).toEqual({
type: 'referenceArray',
props: { source: 'foo_ids', reference: 'foos' },
children: { type: 'referenceArrayChild' },
});
});
it('should return a reference array field for field named *Ids', () => {
expect(inferTypeFromValues('fooIds', ['foo', 'bar'])).toEqual({
type: 'referenceArray',
props: { source: 'fooIds', reference: 'foos' },
children: { type: 'referenceArrayChild' },
});
});
it('should return a string field for no values', () => {
expect(inferTypeFromValues('foo', [])).toEqual({
type: 'string',
props: { source: 'foo' },
});
});
it('should return an array field for array of object values', () => {
expect(
inferTypeFromValues('foo', [
[{ bar: 1 }, { bar: 2 }],
[{ bar: 3 }, { bar: 4 }],
])
).toEqual({
type: 'array',
props: { source: 'foo' },
children: [{ type: 'number', props: { source: 'bar' } }],
});
});
it('should return a string field for array of non-object values', () => {
expect(
inferTypeFromValues('foo', [
[1, 2],
[3, 4],
])
).toEqual({
type: 'string',
props: { source: 'foo' },
});
});
it('should return a boolean field for boolean values', () => {
expect(inferTypeFromValues('foo', [true, false, true])).toEqual({
type: 'boolean',
props: { source: 'foo' },
});
});
it('should return a date field for date values', () => {
expect(
inferTypeFromValues('foo', [
new Date('2018-10-01'),
new Date('2018-12-03'),
])
).toEqual({
type: 'date',
props: { source: 'foo' },
});
});
it('should return an email field for email name', () => {
expect(inferTypeFromValues('email', ['whatever'])).toEqual({
type: 'email',
props: { source: 'email' },
});
});
it.skip('should return an email field for email string values', () => {
expect(
inferTypeFromValues('foo', ['me@example.com', 'you@foo.co.uk'])
).toEqual({
type: 'email',
props: { source: 'foo' },
});
});
it('should return an url field for url name', () => {
expect(inferTypeFromValues('url', ['whatever', 'whatever'])).toEqual({
type: 'url',
props: { source: 'url' },
});
});
it.skip('should return an url field for url string values', () => {
expect(
inferTypeFromValues('foo', [
'http://foo.com/bar',
'https://www.foo.com/index.html#foo',
])
).toEqual({
type: 'url',
props: { source: 'foo' },
});
});
it('should return a date field for date string values', () => {
expect(
inferTypeFromValues('foo', ['2018-10-01', '2018-12-03'])
).toEqual({
type: 'date',
props: { source: 'foo' },
});
});
it('should return a rich text field for HTML values', () => {
expect(
inferTypeFromValues('foo', [
'This is <h1>Good</h1>',
'<body><h1>hello</h1>World</body>',
])
).toEqual({
type: 'richText',
props: { source: 'foo' },
});
});
it('should return a string field for string values', () => {
expect(
inferTypeFromValues('foo', ['This is Good', 'hello, World!'])
).toEqual({
type: 'string',
props: { source: 'foo' },
});
});
it('should return a number field for number values', () => {
expect(inferTypeFromValues('foo', [12, 1e23, 653.56])).toEqual({
type: 'number',
props: { source: 'foo' },
});
});
it('should return a typed field for object values', () => {
expect(
inferTypeFromValues('foo', [
{ bar: 1, baz: 2 },
{ bar: 3, baz: 4 },
])
).toEqual({
type: 'number',
props: { source: 'foo.bar' },
});
});
});

0 comments on commit 19b48ad

Please sign in to comment.