-
-
Notifications
You must be signed in to change notification settings - Fork 31
/
AttributeType.js
201 lines (174 loc) · 5.37 KB
/
AttributeType.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// @flow
import type { Node } from 'react';
type TypeMetadata = { required: boolean };
// eslint-disable-next-line no-use-before-define
export type TypeMap = { [string]: TypeImpl };
type EnumSchema = { [string]: string };
type AttributeSchema = TypeMap | EnumSchema;
// this is facetious, but we need something like it
type TypeType = 'primitive' | 'enum' | 'complex';
const defaultMetadata: TypeMetadata = { required: false };
export interface AttributeType {
/**
* Runtime unique id. Maybe we should just use name.
*/
id: Symbol;
/**
* name of this type. as convention, use all lowercase for primitives and PascalCase for complex
*/
name: string;
typeDescription: Node;
/**
* We are overloading this type concept for the moment and putting the attribute description in this field.
* Usually done through {@link TypeImpl.desc} in an attribute type schema.
*/
description: ?Node;
/**
* the attribute schema that describes this type. null for primitives
*/
schema: ?AttributeSchema;
/**
* Bag for random type configuration
*/
metadata: TypeMetadata;
/**
* The type of this type. A questionable abstraction, but convenient to support enums
*/
type: TypeType;
defaultValue: any;
parse: any => any;
serialize: ?(any) => any;
alias?: ?string;
}
export class TypeImpl implements AttributeType {
id: Symbol;
name: string;
description: ?Node;
typeDescription: Node;
schema: ?AttributeSchema;
metadata: TypeMetadata;
type: TypeType;
defaultValue: any;
parse: any => any;
serialize: ?(any) => any;
alias: ?string;
constructor(data: AttributeType) {
this.id = data.id;
this.name = data.name;
this.schema = data.schema;
this.metadata = data.metadata;
this.type = data.type;
this.defaultValue = data.defaultValue;
this.description = data.description;
this.parse = data.parse;
this.typeDescription = data.typeDescription;
this.serialize = data.serialize;
this.alias = data.alias;
}
/**
* Describe this type. See note about how desc is being overloaded as attribute.
* @param d - description of this attribute
* @returns {TypeImpl} - A new instance of this type, with the description
*/
desc = (d: Node): TypeImpl => new TypeImpl({ ...this, description: d });
aliased = (name: string, typeDescription?: Node): TypeImpl =>
typeDescription
? new TypeImpl({ ...this, name, typeDescription, alias: name })
: new TypeImpl({ ...this, name, alias: name });
meta = (metadata: Object): TypeImpl => new TypeImpl({ ...this, metadata });
isA = (t: AttributeType) => t.id === this.id;
isPrimitive = (): boolean => this.type === 'primitive' && !this.alias;
isUndescribedPrimitive = (): boolean => this.type === 'primitive' && !this.typeDescription;
isComplex = (): boolean => this.type === 'complex';
/**
* See comment about how type / desc is being overloaded above
*/
hasHelpInfo = (): boolean => this.description !== this.typeDescription;
}
export class TypeBuilder {
name: string;
schema: ?AttributeSchema;
metadata: TypeMetadata;
type: TypeType;
defaultValue: any;
description: ?Node;
typeDescription: Node;
serializer: ?(any) => any;
parse: any => any = v => v;
constructor(name: string) {
this.name = name;
}
withMetadata(metadata: TypeMetadata): TypeBuilder {
if (this.metadata) {
throw new Error(
"you likely don't mean to replace the metadata you are building; use `withMetadataAttr`."
);
}
this.metadata = metadata;
return this;
}
withMetadataAttr(attr: $Shape<TypeMetadata>): TypeBuilder {
this.metadata = { ...this.metadata, ...attr };
return this;
}
withParser(parser: any => any): TypeBuilder {
this.parse = parser;
return this;
}
withSerializer(serializer: any => any): TypeBuilder {
this.serializer = serializer;
return this;
}
withType(type: TypeType): TypeBuilder {
this.type = type;
return this;
}
withSchema(schema: AttributeSchema): TypeBuilder {
this.schema = schema;
return this;
}
havingDefault(val: any): TypeBuilder {
this.defaultValue = val;
return this;
}
desc(des: Node): TypeBuilder {
this.typeDescription = des;
return this;
}
validate = () => {
const { name, schema, type } = this;
if (!name) throw new Error('need a name');
// we may want to just key on id and allow duplicate names
if (name in typeRegistry) throw new Error('already exists');
if (!type) throw new Error('need a type');
if (type === 'primitive') {
if (schema) throw new Error('primitives cannot have schemas');
} else {
// again, this type is a cop-out but practically useful
if (name !== 'object' && !schema) throw new Error('non primitives need a schema');
// may want to validate enum / complex schema shape
}
};
build(): TypeImpl {
this.validate();
const t = new TypeImpl({
id: Symbol(),
name: this.name,
schema: this.schema,
metadata: this.metadata || defaultMetadata,
type: this.type,
defaultValue: this.defaultValue,
description: this.description,
typeDescription: this.typeDescription,
parse: this.parse,
serialize: this.serializer,
});
registerType(t);
return t;
}
}
const typeRegistry: TypeMap = {};
window.Types = typeRegistry;
function registerType(t: TypeImpl) {
typeRegistry[t.name] = t;
}