Skip to content

Custom Decorators

Andrew R. edited this page Jul 24, 2026 · 1 revision

Custom Decorators

There is no supported compiler custom-decorator factory or registration API. shamooc recognizes decorators imported from documented Shamoo packages by resolved exported symbol name. A local function that records similar-looking data is not compiler-authoritative metadata, and writing directly to compiler JSON is unsupported.

Safe reflection-only metadata

A project may define runtime-only metadata for its own reflection code. Keep it under a project-owned symbol, do not use SHAMOO_DECLARATIONS, and do not expect shamooc, the bundler, or ShamooRuntime to act on it.

const AUDIT_METADATA = Symbol('example.audit.metadata');

interface AuditMetadata {
  readonly category: string;
}

const auditMetadata = new WeakMap<object, Map<string | symbol, AuditMetadata>>();

export function Audited(category: string): MethodDecorator {
  return (target, propertyKey) => {
    const owner = target.constructor;
    const methods = auditMetadata.get(owner) ?? new Map();
    methods.set(propertyKey, Object.freeze({ category }));
    auditMetadata.set(owner, methods);
  };
}

export function getAuditMetadata(
  owner: object,
): ReadonlyMap<string | symbol, AuditMetadata> {
  return new Map(auditMetadata.get(owner) ?? []);
}

This data is process-local and reflection-only. It must not select arbitrary methods from untrusted strings or be serialized as executable authority. Shamoo's supported declaration reader, getRuntimeDeclarations() from @shamoo/reflection, reads only official Shamoo decorator declarations.

Supported extension interfaces

Use the framework's explicit interfaces instead of inventing compiler decorators:

import type { ExceptionFilter } from '@shamoo/filters';
import type { Guard } from '@shamoo/guards';
import type { Interceptor } from '@shamoo/interceptors';
import type { Pipe } from '@shamoo/pipes';
import type { ValidationIssue, Validator } from '@shamoo/validation';
class AdminGuard implements Guard {
  public canActivate(context: import('@shamoo/interceptors').InvocationContext): boolean {
    return context.attributes.get('role') === 'admin';
  }
}

class TrimPipe implements Pipe<unknown, unknown> {
  public transform(value: unknown): unknown {
    return typeof value === 'string' ? value.trim() : value;
  }
}

class TimingInterceptor implements Interceptor {
  public async intercept(
    context: import('@shamoo/interceptors').InvocationContext,
  ): Promise<unknown> {
    const started = Date.now();
    try {
      return await context.proceed();
    } finally {
      console.info(`invocation took ${String(Date.now() - started)}ms`);
    }
  }
}

class LogFilter implements ExceptionFilter {
  public catch(error: unknown): never {
    console.error(error);
    throw error;
  }
}

class NonEmptyValidator implements Validator<string> {
  public validate(value: string): readonly ValidationIssue[] {
    return value.length === 0
      ? [{ path: [], message: 'must not be empty' }]
      : [];
  }
}

Official decorators can record these extension types:

import {
  Argument,
  Catch,
  Component,
  UseGuards,
  UseInterceptors,
  UsePipes,
  Validate,
} from '@shamoo/decorators';

@Component()
@UseGuards(AdminGuard)
@UseInterceptors(TimingInterceptor)
@Catch(LogFilter)
export class Handler {
  @UsePipes(TrimPipe)
  public run(
    @Argument('value') @Validate(NonEmptyValidator) value: string,
  ): void {}
}

Current wiring caveat

The interfaces and platform-neutral execution helpers are implemented: guards run in declaration order, pipes transform sequentially, interceptors enter in order and unwind in reverse, validators collect issues, and filters may recover or rethrow. InvocationRuntime can run them when an embedding host explicitly resolves instances and builds invocation descriptors.

The current bundled TypeScript runtime adapter does not instantiate these classes from compiler metadata and does not route registered Paper/Velocity callbacks through InvocationRuntime. It invokes handler methods directly. Therefore official @UseGuards, @UsePipes, @UseInterceptors, @Catch, and @Validate declarations are discoverable but do not enforce policy in a normally deployed bundle today. Do not use them as a security boundary until the embedding host explicitly wires the full pipeline.

Sources: decorator implementation, invocation pipeline, and bundled adapter.

Clone this wiki locally