Skip to content

Commit

Permalink
Update to work with standard ES decorators
Browse files Browse the repository at this point in the history
  • Loading branch information
james-pre committed Sep 13, 2024
1 parent 8f08b8f commit 7e1cb4b
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 57 deletions.
52 changes: 40 additions & 12 deletions src/internal/struct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ export interface MemberInit {
length?: number;
}

export const init = Symbol('struct_init');

export type init = typeof init;
export const init: unique symbol = Symbol('struct_init');

/**
* Options for struct initialization
Expand All @@ -31,23 +29,53 @@ export interface Metadata {
size: number;
}

export const metadata = Symbol('struct');
export const metadata: unique symbol = Symbol('struct');

export type metadata = typeof metadata;
export interface _DecoratorMetadata<T extends Metadata = Metadata> extends DecoratorMetadata {
[metadata]?: T;
[init]?: MemberInit[];
}

export interface DecoratorContext<T extends Metadata = Metadata> {
metadata: _DecoratorMetadata<T>;
}

export type MemberContext = ClassMemberDecoratorContext & DecoratorContext;

export interface Static<T extends Metadata = Metadata> {
[metadata]: T;
new (): Instance;
prototype: Instance;
[Symbol.metadata]: DecoratorMetadata & {
[metadata]: T;
};
new (): Instance<T>;
prototype: Instance<T>;
}

export interface StaticLike<T extends Metadata = Metadata> extends ClassLike {
[metadata]?: T;
[init]?: MemberInit[];
[Symbol.metadata]?: _DecoratorMetadata<T> | null;
}

export function isValidMetadata<T extends Metadata = Metadata>(
arg: unknown
): arg is DecoratorMetadata & {
[metadata]: T;
} {
return arg != null && typeof arg == 'object' && metadata in arg;
}

/**
* Gets a reference to Symbol.metadata, even on platforms that do not expose it globally (like Node)
*/
export function symbol_metadata(arg: ClassLike): typeof Symbol.metadata {
const symbol_metadata = Symbol.metadata || Object.getOwnPropertySymbols(arg).find(s => s.description == 'Symbol.metadata');
if (!symbol_metadata) {
throw new ReferenceError('Could not get a reference to Symbol.metadata');
}

return symbol_metadata as typeof Symbol.metadata;
}

export function isStatic<T extends Metadata = Metadata>(arg: unknown): arg is Static<T> {
return typeof arg == 'function' && metadata in arg;
return typeof arg == 'function' && symbol_metadata(arg as ClassLike) in arg && isValidMetadata(arg[symbol_metadata(arg as ClassLike)]);
}

export interface Instance<T extends Metadata = Metadata> {
Expand All @@ -59,7 +87,7 @@ export interface InstanceLike<T extends Metadata = Metadata> {
}

export function isInstance<T extends Metadata = Metadata>(arg: unknown): arg is Instance<T> {
return metadata in (arg?.constructor || {});
return arg != null && typeof arg == 'object' && isStatic(arg.constructor);
}

export function isStruct<T extends Metadata = Metadata>(arg: unknown): arg is Instance<T> | Static<T> {
Expand Down
91 changes: 47 additions & 44 deletions src/struct.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
import * as Struct from './internal/struct.js';
import * as primitive from './internal/primitives.js';
import {
DecoratorContext,
InstanceLike,
MemberInit,
Metadata,
Options,
Size,
StaticLike,
symbol_metadata,
init,
isInstance,
isStatic,
isStruct,
metadata,
type MemberContext,
} from './internal/struct.js';
import { capitalize } from './string.js';
import { ClassLike } from './types.js';
import * as primitive from './internal/primitives.js';
export { Struct };
export * as Struct from './internal/struct.js';

/**
* Gets the size in bytes of a type
*/
export function sizeof<T extends primitive.Valid | Struct.StaticLike | Struct.InstanceLike>(type: T): Struct.Size<T> {
export function sizeof<T extends primitive.Valid | StaticLike | InstanceLike>(type: T): Size<T> {
// primitive
if (typeof type == 'string') {
if (!primitive.isValid(type)) {
throw new TypeError('Invalid primitive type: ' + type);
}

return (+primitive.normalize(type).match(primitive.regex)![2] / 8) as Struct.Size<T>;
return (+primitive.normalize(type).match(primitive.regex)![2] / 8) as Size<T>;
}

if (!Struct.isStruct(type)) {
if (!isStruct(type)) {
throw new TypeError('Not a struct');
}

const meta: Struct.Metadata = Struct.isStatic(type) ? type[Struct.metadata] : type.constructor[Struct.metadata];
const struct = isStatic(type) ? type : type.constructor;

return meta.size as Struct.Size<T>;
return struct[symbol_metadata(struct)][metadata].size as Size<T>;
}

/**
Expand All @@ -36,14 +51,15 @@ export function align(value: number, alignment: number): number {
/**
* Decorates a class as a struct
*/
export function struct(options: Partial<Struct.Options> = {}) {
export function struct(options: Partial<Options> = {}) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return function (target: Struct.StaticLike, _?: ClassDecoratorContext) {
target[Struct.init] ||= [];
return function __decorateStruct<const T extends StaticLike>(target: T, context: ClassDecoratorContext & DecoratorContext): T {
context.metadata[init] ||= [];
let size = 0;
const members = new Map();
for (const { name, type, length } of target[Struct.init]) {
if (!primitive.isValid(type) && !Struct.isStatic(type)) {
for (const _ of context.metadata[init]) {
const { name, type, length } = _;
if (!primitive.isValid(type) && !isStatic(type)) {
throw new TypeError('Not a valid type: ' + type);
}
members.set(name, {
Expand All @@ -55,16 +71,17 @@ export function struct(options: Partial<Struct.Options> = {}) {
size = align(size, options.align || 1);
}

target[Struct.metadata] = { options, members, size } satisfies Struct.Metadata;
context.metadata[metadata] = { options, members, size } satisfies Metadata;
return target;
};
}

/**
* Decorates a class member to be serialized
*/
export function member(type: primitive.Valid | ClassLike, length?: number) {
return function (target: object, context?: ClassMemberDecoratorContext | string | symbol) {
let name = typeof context == 'object' ? context.name : context;
return function <V>(value: V, context: MemberContext): V {
let name = context.name;
if (typeof name == 'symbol') {
console.warn('Symbol used for struct member name will be coerced to string: ' + name.toString());
name = name.toString();
Expand All @@ -74,29 +91,20 @@ export function member(type: primitive.Valid | ClassLike, length?: number) {
throw new ReferenceError('Invalid name for struct member');
}

if (typeof target != 'object') {
throw new TypeError('Invalid member for struct field');
}

if (!('constructor' in target)) {
throw new TypeError('Invalid member for struct field');
}

const struct = (target as Struct.InstanceLike).constructor;

struct[Struct.init] ||= [];
struct[Struct.init].push({ name, type, length } satisfies Struct.MemberInit);
context.metadata[init] ||= [];
context.metadata[init].push({ name, type, length } satisfies MemberInit);
return value;
};
}

/**
* Serializes a struct into a Uint8Array
*/
export function serialize(instance: unknown): Uint8Array {
if (!Struct.isInstance(instance)) {
if (!isInstance(instance)) {
throw new TypeError('Can not serialize, not a struct instance');
}
const { options, members } = instance.constructor[Struct.metadata];
const { options, members } = instance.constructor[symbol_metadata(instance.constructor)][metadata];

const buffer = new Uint8Array(sizeof(instance));
const view = new DataView(buffer.buffer);
Expand Down Expand Up @@ -139,10 +147,10 @@ export function serialize(instance: unknown): Uint8Array {
* Deserializes a struct from a Uint8Array
*/
export function deserialize(instance: unknown, _buffer: ArrayBuffer | ArrayBufferView) {
if (!Struct.isInstance(instance)) {
if (!isInstance(instance)) {
throw new TypeError('Can not deserialize, not a struct instance');
}
const { options, members } = instance.constructor[Struct.metadata];
const { options, members } = instance.constructor[symbol_metadata(instance.constructor)][metadata];

const buffer = new Uint8Array('buffer' in _buffer ? _buffer.buffer : _buffer);

Expand Down Expand Up @@ -191,22 +199,17 @@ export function deserialize(instance: unknown, _buffer: ArrayBuffer | ArrayBuffe
}
}

/**
* Also can be a name when legacy decorators are used
*/
type Context = string | symbol | ClassMemberDecoratorContext;

function _member<T extends primitive.Valid>(type: T) {
function _(length: number): (target: object, context?: Context) => void;
function _(target: object, context?: Context): void;
function _(targetOrLength: object | number, context?: Context) {
if (typeof targetOrLength == 'number') {
return member(type, targetOrLength);
function _structMemberDecorator<const V>(length: number): (value: V, context: MemberContext) => V;
function _structMemberDecorator<const V>(value: V, context: MemberContext): V;
function _structMemberDecorator<const V>(valueOrLength: V | number, context?: MemberContext): V | ((value: V, context: MemberContext) => V) {
if (typeof valueOrLength == 'number') {
return member(type, valueOrLength);
}

return member(type)(targetOrLength, context);
return member(type)(valueOrLength, context!);
}
return _;
return _structMemberDecorator;
}

/**
Expand Down
1 change: 0 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"esModuleInterop": true,
"noImplicitThis": true,
"declaration": true,
"experimentalDecorators": true,
"strict": true
},
"typedocOptions": {
Expand Down

0 comments on commit 7e1cb4b

Please sign in to comment.