-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathFromJsConstructor.ts
153 lines (137 loc) · 5.25 KB
/
FromJsConstructor.ts
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
import { dom, IonTypes } from "../Ion";
import { _hasValue } from "../util";
import { Constructor, Value } from "./Value";
// Converts the provided Iterable into a Set. If no iterable is provided, returns an empty set.
function _newSet<T>(values?: Iterable<T>): Set<T> {
if (_hasValue(values)) {
return new Set<T>(values);
}
return new Set<T>();
}
/**
* Builder to configure and instantiate FromJsConstructor objects. See the documentation for
* the FromJsConstructor class for a description of each field.
*
* Package-visible for use in dom.Value subclass definitions.
* @private
*/
export class FromJsConstructorBuilder {
private _primitives: Set<string>;
private _classesToUnbox: Set<Constructor>;
private _classes: Set<Constructor>;
constructor() {
this._primitives = _newSet();
this._classesToUnbox = _newSet();
this._classes = _newSet();
}
withPrimitives(...primitives: string[]): FromJsConstructorBuilder {
this._primitives = _newSet(primitives);
return this;
}
withClasses(...classes: Constructor[]): FromJsConstructorBuilder {
this._classes = _newSet(classes);
return this;
}
withClassesToUnbox(...classes: Constructor[]): FromJsConstructorBuilder {
this._classesToUnbox = _newSet(classes);
return this;
}
build(): FromJsConstructor {
return new FromJsConstructor(
this._primitives,
this._classesToUnbox,
this._classes
);
}
}
/**
* Provides common conversion and validation logic needed to instantiate subclasses of dom.Value
* from a given Javascript value of unknown type (`any`) and an optional annotations array.
*
* Given a Javascript value, will test its type to see whether it is:
* 1. A primitive that is supported by the specified constructor.
* 2. A boxed primitive that is supported by the specified constructor if unboxed via `valueOf()`.
* 3. A class that is supported by the specified constructor as-is, including boxed primitives.
*
* If the value matches any of the above descriptions, the provided constructor will be invoked
* with the value; otherwise, throws an Error.
*
* Constructors are expected to be compatible with the signature:
*
* constructor(value, annotations: string[]): ClassName
*
* See also: Value._fromJsValue()
*/
export class FromJsConstructor {
/**
* Constructor.
*
* @param _primitives Primitive types that will be passed through as-is.
* @param _classesToUnbox Boxed primitive types that will be converted to primitives via
* `valueOf()` and then passed through.
* @param _classes Classes that will be passed through as-is.
*/
constructor(
private readonly _primitives: Set<string>,
private readonly _classesToUnbox: Set<Constructor>,
private readonly _classes: Set<Constructor>
) {}
/**
* Invokes the provided constructor if `jsValue` is of a supported data type; otherwise
* throws an Error.
*
* @param constructor A dom.Value subclass's constructor to call.
* @param jsValue A Javascript value to validate/unbox before passing through to
* the constructor.
* @param annotations An optional array of strings to associate with the newly constructed
* dom.Value.
*/
construct(constructor: any, jsValue: any, annotations: string[] = []): Value {
if (jsValue === null) {
return new dom.Null(IonTypes.NULL, annotations);
}
const jsValueType = typeof jsValue;
if (jsValueType === "object") {
// jsValue is an unsupported boxed primitive, but we can use it if we convert it to a primitive first
if (this._classesToUnbox.has(jsValue.constructor)) {
return new constructor(jsValue.valueOf(), annotations);
}
// jsValue is an Object of a supported type, including boxed primitives
if (this._classes.has(jsValue.constructor)) {
return new constructor(jsValue, annotations);
}
throw new Error(
`Unable to construct a(n) ${constructor.name} from a ${jsValue.constructor.name}.`
);
}
if (this._primitives.has(jsValueType)) {
return new constructor(jsValue, annotations);
}
throw new Error(
`Unable to construct a(n) ${constructor.name} from a ${jsValueType}.`
);
}
}
// This namespace will be merged with the class definition above. This allows static values to invoke functions in the
// FromJsConstructor class after the class has been fully initialized.
export namespace FromJsConstructor {
// Useful for dom.Value subclasses that do not use a FromJsConstructor (e.g. Struct, List).
// Because it has no supported types, any attempt to use this instance will throw an Error.
export const NONE: FromJsConstructor = new FromJsConstructorBuilder().build();
}
/**
* A mapping of primitive types to the corresponding string that will be returned by
* the `typeof` operator.
*/
export const Primitives = {
/*
* Some possible values are not included because they are unsupported. In particular:
* - "object" indicates that a value is not a primitive.
* - The mapping from Javascript's "symbol" to Ion's type system is not yet clear.
* - No constructors accept "undefined" as a parameter.
*/
Boolean: "boolean",
Number: "number",
String: "string",
BigInt: "bigint",
};