Skip to content

Commit 19b48ad

Browse files
authored
Merge pull request #5485 from marmelab/expose-inference-data
Add ability to infer field type from data
2 parents f0013de + b9196a2 commit 19b48ad

File tree

3 files changed

+337
-0
lines changed

3 files changed

+337
-0
lines changed
+2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import getElementsFromRecords from './getElementsFromRecords';
22
import InferredElement from './InferredElement';
33

4+
export * from './inferTypeFromValues';
5+
46
export { getElementsFromRecords, InferredElement };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import inflection from 'inflection';
2+
3+
import getValuesFromRecords from './getValuesFromRecords';
4+
5+
import {
6+
isObject,
7+
valuesAreArray,
8+
valuesAreBoolean,
9+
valuesAreDate,
10+
valuesAreDateString,
11+
valuesAreHtml,
12+
valuesAreInteger,
13+
valuesAreNumeric,
14+
valuesAreObject,
15+
valuesAreString,
16+
} from './assertions';
17+
18+
const types = [
19+
'array',
20+
'boolean',
21+
'date',
22+
'email',
23+
'id',
24+
'number',
25+
'reference',
26+
'referenceChild',
27+
'referenceArray',
28+
'referenceArrayChild',
29+
'richText',
30+
'string',
31+
'url',
32+
] as const;
33+
34+
export type PossibleInferredElementTypes = typeof types[number];
35+
36+
export interface InferredElementDescription {
37+
type: PossibleInferredElementTypes;
38+
props?: any;
39+
children?: InferredElementDescription | InferredElementDescription[];
40+
}
41+
42+
/**
43+
* Guesses an element type based on an array of values
44+
*
45+
* @example
46+
* inferElementFromValues(
47+
* 'address',
48+
* ['2 Baker Street', '1 Downing street'],
49+
* );
50+
* // { type: 'string', props: { source: 'address' } }
51+
*
52+
* @param {string} name Property name, e.g. 'date_of_birth'
53+
* @param {any[]} values an array of values from which to determine the type, e.g. [12, 34.4, 43]
54+
*/
55+
export const inferTypeFromValues = (
56+
name,
57+
values = []
58+
): InferredElementDescription => {
59+
if (name === 'id') {
60+
return { type: 'id', props: { source: name } };
61+
}
62+
if (name.substr(name.length - 3) === '_id') {
63+
return {
64+
type: 'reference',
65+
props: {
66+
source: name,
67+
reference: inflection.pluralize(
68+
name.substr(0, name.length - 3)
69+
),
70+
},
71+
children: { type: 'referenceChild' },
72+
};
73+
}
74+
if (name.substr(name.length - 2) === 'Id') {
75+
return {
76+
type: 'reference',
77+
props: {
78+
source: name,
79+
reference: inflection.pluralize(
80+
name.substr(0, name.length - 2)
81+
),
82+
},
83+
children: { type: 'referenceChild' },
84+
};
85+
}
86+
if (name.substr(name.length - 4) === '_ids') {
87+
return {
88+
type: 'referenceArray',
89+
props: {
90+
source: name,
91+
reference: inflection.pluralize(
92+
name.substr(0, name.length - 4)
93+
),
94+
},
95+
children: { type: 'referenceArrayChild' },
96+
};
97+
}
98+
if (name.substr(name.length - 3) === 'Ids') {
99+
return {
100+
type: 'referenceArray',
101+
props: {
102+
source: name,
103+
reference: inflection.pluralize(
104+
name.substr(0, name.length - 3)
105+
),
106+
},
107+
children: { type: 'referenceArrayChild' },
108+
};
109+
}
110+
if (values.length === 0) {
111+
if (name === 'email') {
112+
return { type: 'email', props: { source: name } };
113+
}
114+
if (name === 'url') {
115+
return { type: 'url', props: { source: name } };
116+
}
117+
// FIXME introspect further using name
118+
return { type: 'string', props: { source: name } };
119+
}
120+
if (valuesAreArray(values)) {
121+
if (isObject(values[0][0])) {
122+
const leafValues = getValuesFromRecords(
123+
values.reduce((acc, vals) => acc.concat(vals), [])
124+
);
125+
// FIXME bad visual representation
126+
return {
127+
type: 'array',
128+
props: { source: name },
129+
children: Object.keys(leafValues).map(leafName =>
130+
inferTypeFromValues(leafName, leafValues[leafName])
131+
),
132+
};
133+
}
134+
// FIXME introspect further
135+
return { type: 'string', props: { source: name } };
136+
}
137+
if (valuesAreBoolean(values)) {
138+
return { type: 'boolean', props: { source: name } };
139+
}
140+
if (valuesAreDate(values)) {
141+
return { type: 'date', props: { source: name } };
142+
}
143+
if (valuesAreString(values)) {
144+
if (name === 'email') {
145+
return { type: 'email', props: { source: name } };
146+
}
147+
if (name === 'url') {
148+
return { type: 'url', props: { source: name } };
149+
}
150+
if (valuesAreDateString(values)) {
151+
return { type: 'date', props: { source: name } };
152+
}
153+
if (valuesAreHtml(values)) {
154+
return { type: 'richText', props: { source: name } };
155+
}
156+
return { type: 'string', props: { source: name } };
157+
}
158+
if (valuesAreInteger(values) || valuesAreNumeric(values)) {
159+
return { type: 'number', props: { source: name } };
160+
}
161+
if (valuesAreObject(values)) {
162+
// we need to go deeper
163+
// Arbitrarily, choose the first prop of the first object
164+
const propName = Object.keys(values[0]).shift();
165+
const leafValues = values.map(v => v[propName]);
166+
return inferTypeFromValues(`${name}.${propName}`, leafValues);
167+
}
168+
return { type: 'string', props: { source: name } };
169+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import expect from 'expect';
2+
import { inferTypeFromValues } from './inferTypeFromValues';
3+
4+
describe('inferTypeFromValues', () => {
5+
it('should return an InferredElement', () => {
6+
expect(inferTypeFromValues('id', ['foo'])).toEqual({
7+
type: 'id',
8+
props: { source: 'id' },
9+
});
10+
});
11+
it('should return an id field for field named id', () => {
12+
expect(inferTypeFromValues('id', ['foo', 'bar'])).toEqual({
13+
type: 'id',
14+
props: { source: 'id' },
15+
});
16+
});
17+
it('should return a reference field for field named *_id', () => {
18+
expect(inferTypeFromValues('foo_id', ['foo', 'bar'])).toEqual({
19+
type: 'reference',
20+
props: { source: 'foo_id', reference: 'foos' },
21+
children: { type: 'referenceChild' },
22+
});
23+
});
24+
it('should return a reference field for field named *Id', () => {
25+
expect(inferTypeFromValues('fooId', ['foo', 'bar'])).toEqual({
26+
type: 'reference',
27+
props: { source: 'fooId', reference: 'foos' },
28+
children: { type: 'referenceChild' },
29+
});
30+
});
31+
it('should return a reference array field for field named *_ids', () => {
32+
expect(inferTypeFromValues('foo_ids', ['foo', 'bar'])).toEqual({
33+
type: 'referenceArray',
34+
props: { source: 'foo_ids', reference: 'foos' },
35+
children: { type: 'referenceArrayChild' },
36+
});
37+
});
38+
it('should return a reference array field for field named *Ids', () => {
39+
expect(inferTypeFromValues('fooIds', ['foo', 'bar'])).toEqual({
40+
type: 'referenceArray',
41+
props: { source: 'fooIds', reference: 'foos' },
42+
children: { type: 'referenceArrayChild' },
43+
});
44+
});
45+
it('should return a string field for no values', () => {
46+
expect(inferTypeFromValues('foo', [])).toEqual({
47+
type: 'string',
48+
props: { source: 'foo' },
49+
});
50+
});
51+
it('should return an array field for array of object values', () => {
52+
expect(
53+
inferTypeFromValues('foo', [
54+
[{ bar: 1 }, { bar: 2 }],
55+
[{ bar: 3 }, { bar: 4 }],
56+
])
57+
).toEqual({
58+
type: 'array',
59+
props: { source: 'foo' },
60+
children: [{ type: 'number', props: { source: 'bar' } }],
61+
});
62+
});
63+
it('should return a string field for array of non-object values', () => {
64+
expect(
65+
inferTypeFromValues('foo', [
66+
[1, 2],
67+
[3, 4],
68+
])
69+
).toEqual({
70+
type: 'string',
71+
props: { source: 'foo' },
72+
});
73+
});
74+
it('should return a boolean field for boolean values', () => {
75+
expect(inferTypeFromValues('foo', [true, false, true])).toEqual({
76+
type: 'boolean',
77+
props: { source: 'foo' },
78+
});
79+
});
80+
it('should return a date field for date values', () => {
81+
expect(
82+
inferTypeFromValues('foo', [
83+
new Date('2018-10-01'),
84+
new Date('2018-12-03'),
85+
])
86+
).toEqual({
87+
type: 'date',
88+
props: { source: 'foo' },
89+
});
90+
});
91+
it('should return an email field for email name', () => {
92+
expect(inferTypeFromValues('email', ['whatever'])).toEqual({
93+
type: 'email',
94+
props: { source: 'email' },
95+
});
96+
});
97+
it.skip('should return an email field for email string values', () => {
98+
expect(
99+
inferTypeFromValues('foo', ['me@example.com', 'you@foo.co.uk'])
100+
).toEqual({
101+
type: 'email',
102+
props: { source: 'foo' },
103+
});
104+
});
105+
it('should return an url field for url name', () => {
106+
expect(inferTypeFromValues('url', ['whatever', 'whatever'])).toEqual({
107+
type: 'url',
108+
props: { source: 'url' },
109+
});
110+
});
111+
it.skip('should return an url field for url string values', () => {
112+
expect(
113+
inferTypeFromValues('foo', [
114+
'http://foo.com/bar',
115+
'https://www.foo.com/index.html#foo',
116+
])
117+
).toEqual({
118+
type: 'url',
119+
props: { source: 'foo' },
120+
});
121+
});
122+
it('should return a date field for date string values', () => {
123+
expect(
124+
inferTypeFromValues('foo', ['2018-10-01', '2018-12-03'])
125+
).toEqual({
126+
type: 'date',
127+
props: { source: 'foo' },
128+
});
129+
});
130+
it('should return a rich text field for HTML values', () => {
131+
expect(
132+
inferTypeFromValues('foo', [
133+
'This is <h1>Good</h1>',
134+
'<body><h1>hello</h1>World</body>',
135+
])
136+
).toEqual({
137+
type: 'richText',
138+
props: { source: 'foo' },
139+
});
140+
});
141+
it('should return a string field for string values', () => {
142+
expect(
143+
inferTypeFromValues('foo', ['This is Good', 'hello, World!'])
144+
).toEqual({
145+
type: 'string',
146+
props: { source: 'foo' },
147+
});
148+
});
149+
it('should return a number field for number values', () => {
150+
expect(inferTypeFromValues('foo', [12, 1e23, 653.56])).toEqual({
151+
type: 'number',
152+
props: { source: 'foo' },
153+
});
154+
});
155+
it('should return a typed field for object values', () => {
156+
expect(
157+
inferTypeFromValues('foo', [
158+
{ bar: 1, baz: 2 },
159+
{ bar: 3, baz: 4 },
160+
])
161+
).toEqual({
162+
type: 'number',
163+
props: { source: 'foo.bar' },
164+
});
165+
});
166+
});

0 commit comments

Comments
 (0)