Skip to content

Commit 7a0ed25

Browse files
committed
refactor: simplify schema type
1 parent 240abe0 commit 7a0ed25

File tree

5 files changed

+277
-150
lines changed

5 files changed

+277
-150
lines changed

packages/types/src/builtins/collections.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
1-
import {RealType} from './common.js';
2-
import {BaseType, MapType, ArrayType, Type} from '../interfaces.js';
1+
import {createBranchConstructor, RealType, TypeContext} from './common.js';
2+
import {
3+
BaseSchema,
4+
BaseType,
5+
MapSchema,
6+
MapType,
7+
ArrayType,
8+
Type,
9+
} from '../interfaces.js';
10+
import {
11+
printJSON as j,
12+
} from '../utils.js';
313

4-
/** Avro map. Represented as vanilla objects. */
5-
class RealMapType extends RealType implements MapType {
14+
/** Avro map. Represented as maps. */
15+
export class RealMapType extends RealType implements MapType {
616
override readonly typeName = 'map';
17+
readonly valuesType: Type;
18+
protected readonly branchConstructor = createBranchConstructor(
719

8-
constructor(schema, opts) {
9-
super();
20+
constructor(schema: MapSchema, ctx: TypeContext) {
21+
super(schema, ctx);
1022
if (!schema.values) {
1123
throw new Error(`missing map values: ${j(schema)}`);
1224
}
13-
this.valuesType = Type.forSchema(schema.values, opts);
25+
this.valuesType = ctx.factory(schema.values, {
26+
...ctx,
27+
depth: ctx.depth + 1,
28+
});
1429
this._branchConstructor = this._createBranchConstructor();
1530
Object.freeze(this);
1631
}
@@ -98,7 +113,7 @@ class RealMapType extends RealType implements MapType {
98113
throw new Error('maps cannot be compared');
99114
}
100115

101-
override _match() {
116+
override _match(): never {
102117
return this.compare();
103118
}
104119

@@ -123,17 +138,13 @@ class RealMapType extends RealType implements MapType {
123138
throw invalidValueError(val, this);
124139
}
125140

126-
valuesType(): Type {
127-
return this.valuesType;
128-
}
129-
130141
_deref(schema, derefed, opts) {
131142
schema.values = this.valuesType._attrs(derefed, opts);
132143
}
133144
}
134145

135146
/** Avro array. Represented as vanilla arrays. */
136-
class RealArrayType extends RealType implements ArrayType {
147+
export class RealArrayType extends RealType implements ArrayType {
137148
override readonly typeName = 'array';
138149

139150
constructor(schema, opts) {

packages/types/src/builtins/common.ts

Lines changed: 74 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
import {
22
BaseType,
33
BaseSchema,
4+
Branch,
45
ErrorHook,
6+
LogicalType,
57
NamedSchema,
8+
Schema,
69
Type,
10+
TypeCloneOptions,
11+
TypeIsValidOptions,
12+
TypeSchemaOptions,
713
PrimitiveTypeName,
814
primitiveTypeNames,
915
} from '../interfaces.js';
@@ -19,13 +25,37 @@ import {
1925
// Encoding tap (shared for performance).
2026
const TAP = Tap.withCapacity(1024);
2127

28+
export function resizeDefaultBuffer(size: number): void {
29+
TAP.reinitialize(size);
30+
}
31+
2232
// Currently active logical type, used for name redirection.
2333
let activeLogicalType: Type | null = null;
2434

2535
// Underlying types of logical types currently being instantiated. This is used
2636
// to be able to reference names (i.e. for branches) during instantiation.
2737
const activeUnderlyingTypes: [Type, RealType][] = [];
2838

39+
export type TypeRegistry = Map<string, Type>;
40+
41+
export function isType<N extends string>(
42+
arg: unknown,
43+
...prefixes: N[]
44+
): arg is Type & {readonly typeName: `${N}${string}`} {
45+
if (!arg || !(arg instanceof RealType)) {
46+
// Not fool-proof, but most likely good enough.
47+
return false;
48+
}
49+
return prefixes.some((p) => arg.typeName.startsWith(p));
50+
}
51+
52+
export interface TypeContext {
53+
readonly factory: (s: Schema, ctx: TypeContext) => Type;
54+
readonly registry: TypeRegistry;
55+
readonly namespace?: string;
56+
readonly depth: number; // TODO: Use.
57+
}
58+
2959
/**
3060
* "Abstract" base Avro type.
3161
*
@@ -43,7 +73,8 @@ export abstract class RealType<V = any> implements BaseType<V> {
4373
readonly name: string | undefined;
4474
readonly aliases: string[] | undefined;
4575
readonly doc: string | undefined;
46-
protected constructor(schema: BaseSchema | NamedSchema, opts?: TypeOptions) {
76+
protected branchConstructor: BranchConstructor | undefined;
77+
protected constructor(schema: BaseSchema | NamedSchema, ctx: TypeContext) {
4778
let type;
4879
if (activeLogicalType) {
4980
type = activeLogicalType;
@@ -61,7 +92,7 @@ export abstract class RealType<V = any> implements BaseType<V> {
6192
let name = schema.name;
6293
const namespace =
6394
schema.namespace === undefined
64-
? opts && opts.namespace
95+
? ctx.namespace
6596
: schema.namespace;
6697
if (name !== undefined) {
6798
// This isn't an anonymous type.
@@ -70,37 +101,33 @@ export abstract class RealType<V = any> implements BaseType<V> {
70101
// Avro doesn't allow redefining primitive names.
71102
throw new Error(`cannot rename primitive type: ${j(name)}`);
72103
}
73-
const registry = opts && opts.registry;
104+
const registry = ctx.registry;
74105
if (registry) {
75-
if (registry[name] !== undefined) {
106+
if (registry.has(name)) {
76107
throw new Error(`duplicate type name: ${name}`);
77108
}
78-
registry[name] = type;
109+
registry.set(name, type as any);
79110
}
80-
} else if (opts && opts.noAnonymousTypes) {
81-
throw new Error(`missing name property in schema: ${j(schema)}`);
82111
}
83112
this.name = name;
84113
this.aliases = schema.aliases
85-
? schema.aliases.map((s: string) => {
86-
return maybeQualify(s, namespace);
87-
})
114+
? schema.aliases.map((s: string) => maybeQualify(s, namespace))
88115
: [];
89116
}
90117
}
91118

92-
get branchName(): string {
93-
const type = isType(this, 'logical') ? this.underlyingType : this;
94-
if (type.name) {
95-
return type.name;
119+
get branchName(): string | undefined {
120+
let t: Type = this as any;
121+
if (isType(t, 'logical')) {
122+
t = t.underlyingType;
96123
}
97-
if (isType(type, 'abstract')) {
98-
return type._concreteTypeName;
124+
if (t.name) {
125+
return t.name;
99126
}
100-
return isType(type, 'union') ? undefined : type.typeName;
127+
return isType(t, 'union') ? undefined : t.typeName;
101128
}
102129

103-
clone(val: V, opts?: CloneOptions): any {
130+
clone(val: V, opts?: TypeCloneOptions): any {
104131
if (opts) {
105132
opts = {
106133
coerce: !!opts.coerceBuffers | 0, // Coerce JSON to Buffer.
@@ -113,10 +140,10 @@ export abstract class RealType<V = any> implements BaseType<V> {
113140
}
114141
// If no modifications are required, we can get by with a serialization
115142
// roundtrip (generally much faster than a standard deep copy).
116-
return this.fromBuffer(this.toBuffer(val));
143+
return this.binaryDecode(this.binaryEncode(val));
117144
}
118145

119-
compareBuffers(buf1: Uint8Array, buf2: Uint8Array): number {
146+
binaryCompare(buf1: Uint8Array, buf2: Uint8Array): -1 | 0 | 1 {
120147
return this._match(Tap.fromBuffer(buf1), Tap.fromBuffer(buf2));
121148
}
122149

@@ -175,7 +202,7 @@ export abstract class RealType<V = any> implements BaseType<V> {
175202
return Object.freeze(resolver);
176203
}
177204

178-
decode(buf: Uint8Array, pos?: number, resolver?: TypeResolver): V {
205+
binaryDecodeAt(buf: Uint8Array, pos?: number, resolver?: TypeResolver): V {
179206
const tap = Tap.fromBuffer(buf, pos);
180207
const val = readValue(this, tap, resolver);
181208
if (!tap.isValid()) {
@@ -184,7 +211,7 @@ export abstract class RealType<V = any> implements BaseType<V> {
184211
return {value: val, offset: tap.pos};
185212
}
186213

187-
encode(val: V, buf: Uint8Array, pos?: number): number {
214+
binaryEncodeAt(val: V, buf: Uint8Array, pos?: number): number {
188215
const tap = Tap.fromBuffer(buf, pos);
189216
this._write(tap, val);
190217
if (!tap.isValid()) {
@@ -238,9 +265,9 @@ export abstract class RealType<V = any> implements BaseType<V> {
238265
return `<${className} ${j(obj)}>`;
239266
}
240267

241-
isValid(val: V, opts?: IsValidOptions): boolean {
268+
isValid(arg: unknown, opts?: TypeIsValidOptions): boolean {
242269
// We only have a single flag for now, so no need to complicate things.
243-
const flags = (opts && opts.noUndeclaredFields) | 0;
270+
const flags = (opts && opts.allowUndeclaredFields) | 0;
244271
const errorHook = opts && opts.errorHook;
245272
let hook, path;
246273
if (errorHook) {
@@ -249,10 +276,10 @@ export abstract class RealType<V = any> implements BaseType<V> {
249276
errorHook.call(this, path.slice(), any, type, val);
250277
};
251278
}
252-
return this._check(val, flags, hook, path);
279+
return this._check(arg, flags, hook, path);
253280
}
254281

255-
schema(opts?: SchemaOptions): Schema {
282+
schema<E = SchemaExtensions>(opts?: TypeSchemaOptions): Schema<E, never> {
256283
// Copy the options to avoid mutating the original options object when we
257284
// add the registry of dereferenced types.
258285
return this._attrs(
@@ -264,7 +291,7 @@ export abstract class RealType<V = any> implements BaseType<V> {
264291
);
265292
}
266293

267-
toBuffer(val: V): Uint8Array {
294+
binaryEncode(val: V): Uint8Array {
268295
TAP.pos = 0;
269296
this._write(TAP, val);
270297
if (TAP.isValid()) {
@@ -275,17 +302,8 @@ export abstract class RealType<V = any> implements BaseType<V> {
275302
return buf;
276303
}
277304

278-
toJSON(): unknown {
279-
// Convenience to allow using `JSON.stringify(type)` to get a type's schema.
280-
return this.schema({exportAttrs: true});
281-
}
282-
283-
toString(val?: any): string {
284-
if (val === undefined) {
285-
// Consistent behavior with standard `toString` expectations.
286-
return JSON.stringify(this.schema({noDeref: true}));
287-
}
288-
return JSON.stringify(this._copy(val, {coerce: 3}));
305+
jsonEncode(val?: V): unknown {
306+
return this._copy(val, {coerce: 3});
289307
}
290308

291309
wrap(val: any): any {
@@ -331,22 +349,6 @@ export abstract class RealType<V = any> implements BaseType<V> {
331349
return schema;
332350
}
333351

334-
_createBranchConstructor() {
335-
const name = this.branchName;
336-
if (name === 'null') {
337-
return null;
338-
}
339-
const attr = ~name.indexOf('.') ? "this['" + name + "']" : 'this.' + name;
340-
const body = 'return function Branch$(val) { ' + attr + ' = val; };';
341-
342-
const Branch = new Function(body)();
343-
Branch.type = this;
344-
345-
Branch.prototype.unwrap = new Function('return ' + attr + ';');
346-
Branch.prototype.unwrapped = Branch.prototype.unwrap; // Deprecated.
347-
return Branch;
348-
}
349-
350352
_peek(tap: Tap): any {
351353
const pos = tap.pos;
352354
const val = this._read(tap);
@@ -357,7 +359,7 @@ export abstract class RealType<V = any> implements BaseType<V> {
357359
protected abstract compare(obj1: unknown, obj2: unknown): -1 | 0 | 1;
358360

359361
protected abstract _check(
360-
args: unknown,
362+
arg: unknown,
361363
flags: any,
362364
hook: ErrorHook,
363365
path: string[]
@@ -367,19 +369,19 @@ export abstract class RealType<V = any> implements BaseType<V> {
367369

368370
protected abstract _deref(): any;
369371

370-
protected abstract _match(tap1: Tap, tap2: Tap): number;
372+
protected abstract _match(tap1: Tap, tap2: Tap): -1 | 0 | 1;
371373

372374
abstract _read(tap: Tap): V;
373375

374376
protected abstract _skip(tap: Tap): void;
375377

376378
protected abstract _update(resolver: TypeResolver): void;
377379

378-
protected abstract _write(tap: Tap): void;
380+
protected abstract _write(tap: Tap, val: V): void;
379381
}
380382

381383
/** Derived type abstract class. */
382-
abstract class RealLogicalType extends RealType {
384+
export abstract class RealLogicalType extends RealType implements LogicalType {
383385
private _logicalTypeName: string;
384386
constructor(schema: Schema, opts?: TypeOptions) {
385387
super(schema, opts);
@@ -511,10 +513,6 @@ abstract class RealLogicalType extends RealType {
511513
protected abstract _resolve();
512514
}
513515

514-
function __reset(size: number): void {
515-
TAP.reinitialize(size);
516-
}
517-
518516
/** TypeResolver to read a writer's schema as a new schema. */
519517
class TypeResolver {
520518
_read: ((tap: Tap, lazy: boolean) => any) | undefined;
@@ -643,13 +641,19 @@ export function anonymousName(): string {
643641
return 'Anonymous'; // TODO: May unique.
644642
}
645643

646-
export function isType<N extends string>(
647-
arg: unknown,
648-
...prefixes: N[]
649-
): arg is Type & {readonly typeName: `${N}${string}`} {
650-
if (!arg || !(arg instanceof RealType)) {
651-
// Not fool-proof, but most likely good enough.
652-
return false;
644+
type BranchConstructor = (v: unknown) => Branch;
645+
646+
export function createBranchConstructor(t: RealType): BranchConstructor | null {
647+
const name = t.branchName;
648+
assert(name, 'missing name');
649+
if (name === 'null') {
650+
return null;
653651
}
654-
return prefixes.some((p) => arg.typeName.startsWith(p));
652+
const attr = name.includes('.') ? "this['" + name + "']" : 'this.' + name;
653+
const body = 'return function Branch$(val) { ' + attr + ' = val; };';
654+
655+
const Branch = new Function(body)();
656+
Branch.prototype.wrappedType = t;
657+
Branch.prototype.unwrap = new Function('return ' + attr + ';');
658+
return Branch;
655659
}

packages/types/src/builtins/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,10 @@ import {assert, printJSON as j} from '../utils.js';
33
import {isBufferLike} from '../binary.js';
44
import {RealType, anonymousName, isType} from './common.js';
55
import {RealUnwrappedUnionType, RealWrappedUnionType} from './unions.js';
6+
import {RealArrayType, RealMapType} from './collections.js';
67

78
export {isType} from './common.js';
89

9-
function todo(..._args: any): Error {
10-
return new Error('todo');
11-
}
12-
1310
export function parseType<V = Type>(
1411
schema: Schema,
1512
opts?: ParseTypeOptions

0 commit comments

Comments
 (0)