Skip to content

Commit eee1a5f

Browse files
sam-gcavolkovi
authored andcommitted
Add an assertTypes utility (#3578)
* Add assertTypes util * Add assertTypes function
1 parent 8b07476 commit eee1a5f

File tree

2 files changed

+236
-1
lines changed

2 files changed

+236
-1
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/**
2+
* @license
3+
* Copyright 2020 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect } from 'chai';
19+
20+
import { FirebaseError } from '@firebase/util';
21+
22+
import { assertTypes, opt } from './assert';
23+
24+
class Parent {}
25+
class Child extends Parent {}
26+
27+
describe('assertTypes', () => {
28+
context('basic types', () => {
29+
it('works when no arguments are present', () => {
30+
assertTypes([]);
31+
});
32+
33+
it('works using a basic argument', () => {
34+
assertTypes(['foobar'], 'string');
35+
expect(() => assertTypes([46], 'string')).to.throw(
36+
FirebaseError,
37+
'auth/argument-error'
38+
);
39+
expect(() => assertTypes([], 'string')).to.throw(
40+
FirebaseError,
41+
'auth/argument-error'
42+
);
43+
});
44+
45+
it('works using optional types with missing value', () => {
46+
assertTypes([], opt('string'));
47+
assertTypes([35], 'number', opt('string'));
48+
});
49+
50+
it('works using optional types with value set', () => {
51+
assertTypes(['foo'], opt('string'));
52+
expect(() => assertTypes([46], opt('string'))).to.throw(
53+
FirebaseError,
54+
'auth/argument-error'
55+
);
56+
});
57+
58+
it('works with multiple types', () => {
59+
assertTypes(['foo', null], 'string', 'null');
60+
});
61+
62+
it("works with or'd types", () => {
63+
assertTypes(['foo'], 'string|number');
64+
assertTypes([47], 'string|number');
65+
});
66+
67+
it('works with the arguments field from a function', () => {
68+
function test(_name: string, _height?: unknown): void {
69+
assertTypes(arguments, 'string', opt('number'));
70+
}
71+
72+
test('foo');
73+
test('foo', 11);
74+
expect(() => test('foo', 'bar')).to.throw(
75+
FirebaseError,
76+
'auth/argument-error'
77+
);
78+
});
79+
80+
it('works with class types', () => {
81+
assertTypes([new Child()], Child);
82+
assertTypes([new Child()], Parent);
83+
assertTypes([new Parent()], opt(Parent));
84+
expect(() => assertTypes([new Parent()], Child)).to.throw(
85+
FirebaseError,
86+
'auth/argument-error'
87+
);
88+
});
89+
});
90+
91+
context('record types', () => {
92+
it('works one level deep', () => {
93+
assertTypes([{ foo: 'bar', clazz: new Child(), test: null }], {
94+
foo: 'string',
95+
clazz: Parent,
96+
test: 'null',
97+
missing: opt('string')
98+
});
99+
100+
expect(() =>
101+
assertTypes(
102+
[{ foo: 'bar', clazz: new Child(), test: null, missing: 46 }],
103+
{
104+
foo: 'string',
105+
clazz: Parent,
106+
test: 'null',
107+
missing: opt('string')
108+
}
109+
)
110+
).to.throw(FirebaseError, 'auth/argument-error');
111+
});
112+
113+
it('works nested', () => {
114+
assertTypes(
115+
[{ name: 'foo', metadata: { height: 11, extraInfo: null } }],
116+
{
117+
name: 'string',
118+
metadata: {
119+
height: opt('number'),
120+
extraInfo: 'string|null'
121+
}
122+
}
123+
);
124+
125+
expect(() =>
126+
assertTypes(
127+
[{ name: 'foo', metadata: { height: 11, extraInfo: null } }],
128+
{
129+
name: 'string',
130+
metadata: {
131+
height: opt('number'),
132+
extraInfo: 'string'
133+
}
134+
}
135+
)
136+
).to.throw(FirebaseError, 'auth/argument-error');
137+
});
138+
139+
it('works with triply nested', () => {
140+
assertTypes([{ a: { b: { c: 'test' } } }], { a: { b: { c: 'string' } } });
141+
142+
expect(() =>
143+
assertTypes([{ a: { b: { c: 'test' } } }], {
144+
a: { b: { c: 'number' } }
145+
})
146+
).to.throw(FirebaseError, 'auth/argument-error');
147+
});
148+
});
149+
});

packages-exp/auth-exp/src/core/util/assert.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import { AuthErrorCode, AUTH_ERROR_FACTORY } from '../errors';
18+
import { AUTH_ERROR_FACTORY, AuthErrorCode } from '../errors';
1919
import { _logError } from './log';
2020

2121
/**
@@ -44,6 +44,92 @@ export function assert(
4444
}
4545
}
4646

47+
type TypeExpectation = Function | string | MapType;
48+
49+
interface MapType extends Record<string, TypeExpectation | Optional> {}
50+
51+
class Optional {
52+
constructor(readonly type: TypeExpectation) {}
53+
}
54+
55+
export function opt(type: TypeExpectation): Optional {
56+
return new Optional(type);
57+
}
58+
59+
/**
60+
* Asserts the runtime types of arguments. The 'expected' field can be one of
61+
* a class, a string (representing a "typeof" call), or a record map of name
62+
* to type. Furthermore, the opt() function can be used to mark a field as
63+
* optional. For example:
64+
*
65+
* function foo(auth: Auth, profile: {displayName?: string}, update = false) {
66+
* assertTypes(arguments, [AuthImpl, {displayName: opt('string')}, opt('boolean')]);
67+
* }
68+
*
69+
* opt() can be used for any type:
70+
* function foo(auth?: Auth) {
71+
* assertTypes(arguments, [opt(AuthImpl)]);
72+
* }
73+
*
74+
* The string types can be or'd together, and you can use "null" as well (note
75+
* that typeof null === 'object'; this is an edge case). For example:
76+
*
77+
* function foo(profile: {displayName?: string | null}) {
78+
* assertTypes(arguments, [{displayName: opt('string|null')}]);
79+
* }
80+
*
81+
* @param args
82+
* @param expected
83+
*/
84+
export function assertTypes(
85+
args: Omit<IArguments, 'callee'>,
86+
...expected: Array<TypeExpectation | Optional>
87+
): void {
88+
if (args.length > expected.length) {
89+
fail('TODO', AuthErrorCode.ARGUMENT_ERROR);
90+
}
91+
92+
for (let i = 0; i < expected.length; i++) {
93+
let expect = expected[i];
94+
const arg = args[i];
95+
96+
if (expect instanceof Optional) {
97+
// If the arg is undefined, then it matches "optional" and we can move to
98+
// the next arg
99+
if (typeof arg === 'undefined') {
100+
continue;
101+
}
102+
expect = expect.type;
103+
}
104+
105+
if (typeof expect === 'string') {
106+
// Handle the edge case for null because typeof null === 'object'
107+
if (expect.includes('null') && arg === null) {
108+
continue;
109+
}
110+
111+
const required = expect.split('|');
112+
assert(
113+
required.includes(typeof arg),
114+
'TODO',
115+
AuthErrorCode.ARGUMENT_ERROR
116+
);
117+
} else if (typeof expect === 'object') {
118+
// Recursively check record arguments
119+
const record = arg as Record<string, unknown>;
120+
const map = expect as MapType;
121+
const keys = Object.keys(expect);
122+
123+
assertTypes(
124+
keys.map(k => record[k]),
125+
...keys.map(k => map[k])
126+
);
127+
} else {
128+
assert(arg instanceof expect, 'app', AuthErrorCode.ARGUMENT_ERROR);
129+
}
130+
}
131+
}
132+
47133
/**
48134
* Unconditionally fails, throwing an internal error with the given message.
49135
*

0 commit comments

Comments
 (0)