-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5485 from marmelab/expose-inference-data
Add ability to infer field type from data
- Loading branch information
Showing
3 changed files
with
337 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
166
packages/ra-core/src/inference/inferTypesFromValues.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' }, | ||
}); | ||
}); | ||
}); |