Skip to content

Commit

Permalink
Merge pull request #26 from medishen/dev/v2
Browse files Browse the repository at this point in the history
feat(metadata): Add metadata package implementation with core functionality and utility support
  • Loading branch information
0xii00 authored Jan 29, 2025
2 parents 619b92c + 9db2bfe commit 5f5feff
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 0 deletions.
146 changes: 146 additions & 0 deletions packages/metadata/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { Decorator, MetadataKey, MetadataParameterIndex, MetadataTarget, MetadataValue } from './types/metadata.types';
import ReflectStorage from './storage/storage-metadata';
import { constructKey, MetadataScope, MetadataScopeType, resolveTargetClass } from './utils/metadta.utils';
export * from './interface/metadata.interface';
export * from './types/metadata.types';
class Reflector {
private storage: ReflectStorage;
constructor() {
this.storage = new ReflectStorage();
}

/**
* Define metadata for a target and optional property.
*/
defineMetadata<T extends MetadataValue>(metadataKey: MetadataKey, value: T, target: MetadataTarget, propertyKey?: MetadataKey, parameterIndex?: MetadataParameterIndex): void {
const key = constructKey(metadataKey, propertyKey, parameterIndex);
const actualTarget = resolveTargetClass(target);
this.storage.set(actualTarget!, key, value);
}

/**
* Retrieve metadata for a target and optional property.
*/
getMetadata<T = MetadataValue>(metadataKey: MetadataKey, target: MetadataTarget, propertyKey?: MetadataKey, parameterIndex?: MetadataParameterIndex): T | unknown {
const key = constructKey(metadataKey, propertyKey, parameterIndex);
let currentTarget = resolveTargetClass(target);
while (currentTarget) {
const metadataMap = this.storage.get(currentTarget);

if (metadataMap && metadataMap.has(key)) {
return metadataMap.get(key);
}
currentTarget = Object.getPrototypeOf(currentTarget);
}
return undefined;
}

/**
* Check if metadata exists for a target and optional property.
*/
hasMetadata(metadataKey: MetadataKey, target: MetadataTarget, propertyKey?: MetadataKey, parameterIndex?: MetadataParameterIndex): boolean {
const key = constructKey(metadataKey, propertyKey, parameterIndex);
return this.storage.has(target, key);
}

/**
* Delete metadata for a target and optional property.
*/
deleteMetadata(metadataKey: MetadataKey, target: MetadataTarget, propertyKey?: MetadataKey, parameterIndex?: MetadataParameterIndex): boolean {
const key = constructKey(metadataKey, propertyKey, parameterIndex);
const actualTarget = resolveTargetClass(target);
if (!actualTarget) {
return false;
}
return this.storage.delete(actualTarget, key);
}

/**
* Clear all metadata for a target.
*/
clearMetadata(target: MetadataTarget): void {
const actualTarget = resolveTargetClass(target);
if (!actualTarget) {
throw new Error('Unable to clear metadata: The provided target is invalid or not properly defined.');
}
this.storage.clear(actualTarget);
}
// Decorator factory
metadata(key: MetadataKey, value: MetadataValue): Decorator {
return (target: MetadataTarget, propertyKey?: MetadataKey, descriptorOrIndex?: MetadataParameterIndex | TypedPropertyDescriptor<any>) => {
if (typeof descriptorOrIndex === 'number') {
// Parameter decorator (target, prop, index)
this.defineMetadata(key, value, target, propertyKey, descriptorOrIndex);
} else if (typeof descriptorOrIndex === 'object') {
// Method decorator (target, prop, descriptor)
this.defineMetadata(key, value, target, propertyKey);
} else if (typeof propertyKey === 'undefined') {
// Class decorator (target)
this.defineMetadata(key, value, target);
} else {
// Property decorator (target, prop)
this.defineMetadata(key, value, target, propertyKey);
}
};
}
/**
* List all metadata keys for a target and optional property.
*/
getMetadataKeys(target: MetadataTarget, scope?: MetadataScopeType, propertyName?: string, parameterIndex?: number): MetadataKey[] {
const resolvedTarget = resolveTargetClass(target);
const metadataMap = this.storage.get(resolvedTarget!);

if (!metadataMap) {
return [];
}

return Array.from(metadataMap.keys())
.filter((key) => {
const segments = String(key).split(':');
const currentScope = segments[0];

if (scope && currentScope !== MetadataScope[scope.toUpperCase()]) {
return false;
}

if (propertyName && segments.length > 1 && segments[1] !== propertyName) {
return false;
}

if (typeof parameterIndex === 'number') {
const hasParamSegment = segments.length > 3 && segments[2] === 'param';
const hasCorrectIndex = hasParamSegment && Number(segments[3]) === parameterIndex;

if (!hasCorrectIndex) {
return false;
}
}

return true;
})
.map((key) => {
const segments = String(key).split(':');
return segments[segments.length - 1];
});
}

/**
* List all metadata for a target.
*/
listMetadata<K extends MetadataKey, V extends MetadataValue>(target: MetadataTarget): Map<K, V> | null {
const metadataMap = this.storage.list(target);
if (metadataMap) {
return metadataMap as Map<K, V>;
}
return null;
}

/**
* List all metadata across all targets.
*/
listAllMetadata(): Map<string, Map<MetadataKey, MetadataValue>> {
return this.storage.allList();
}
}

export default new Reflector();
12 changes: 12 additions & 0 deletions packages/metadata/interface/metadata.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { MetadataKey, MetadataTarget, MetadataValue } from '../types/metadata.types';

export interface MetadataStorage {
get(target: MetadataTarget): Map<MetadataKey, MetadataValue>;
set(target: MetadataTarget, key: MetadataKey, value: MetadataValue): void;
has(target: MetadataTarget, key: MetadataKey): boolean;
delete(target: MetadataTarget, key: MetadataKey): boolean;
clear(target: MetadataTarget): void;
keys(): string[];
list(target: MetadataTarget): Map<MetadataKey, MetadataValue> | null;
allList(): Map<string, Map<MetadataKey, MetadataValue>>;
}
16 changes: 16 additions & 0 deletions packages/metadata/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@gland/metadata",
"version": "1.0.0",
"author": "Mahdi",
"license": "MIT",
"scripts": {
"build": "tsc"
},
"dependencies": {
"@gland/common": "workspace:*",
"@gland/cache": "workspace:*"
},
"devDependencies": {
"typescript": "^5.5.4"
}
}
92 changes: 92 additions & 0 deletions packages/metadata/storage/storage-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { isClass } from '@gland/common/utils';
import { MemoryCacheStore } from '@gland/cache';
import { MetadataStorage } from '../interface/metadata.interface';
import { MetadataKey, MetadataTarget, MetadataValue } from '../types/metadata.types';

class ReflectStorage implements MetadataStorage {
private storage = new MemoryCacheStore<string, Map<MetadataKey, MetadataValue>>();
constructor() {
this.storage = new MemoryCacheStore({
policy: 'LRU',
});
}
private getTargetIdentifier(target: MetadataTarget): string {
if (typeof target === 'function') {
return isClass(target) ? `class:${target.name}` : `function:${target.name}@${target.length}`;
}
if (target.constructor && target.constructor.prototype === target) {
return `prototype:${target.constructor.name}`;
}
return `instance:${target.constructor.name}`;
}

private ensureMetadataMap(target: MetadataTarget): Map<MetadataKey, MetadataValue> {
const targetKey = this.getTargetIdentifier(target);
let metadataMap = this.storage.get(targetKey);
if (!(metadataMap instanceof Map)) {
metadataMap = new Map<MetadataKey, MetadataValue>();
this.storage.set(targetKey, metadataMap);
}
return metadataMap;
}
get(target: MetadataTarget): Map<MetadataKey, MetadataValue> {
const targetKey = this.getTargetIdentifier(target);
const metadataMap = this.storage.get(targetKey);
return metadataMap instanceof Map ? metadataMap : new Map<MetadataKey, MetadataValue>();
}

set(target: MetadataTarget, key: MetadataKey, value: MetadataValue): void {
const metadataMap = this.ensureMetadataMap(target);
metadataMap.set(key, value);
const targetKey = this.getTargetIdentifier(target);
this.storage.set(targetKey, metadataMap);
}

has(target: MetadataTarget, key: MetadataKey): boolean {
const metadataMap = this.get(target);
return metadataMap.has(key);
}

delete(target: MetadataTarget, key: MetadataKey): boolean {
const targetKey = this.getTargetIdentifier(target);
const metadataMap = this.get(target);
const result = metadataMap.delete(key);
if (metadataMap.size === 0) {
this.storage.delete(targetKey);
} else {
this.storage.set(targetKey, metadataMap);
}

return result;
}
clear(target: MetadataTarget): void {
const targetKey = this.getTargetIdentifier(target);
this.storage.delete(targetKey);
}

list(target: MetadataTarget): Map<MetadataKey, MetadataValue> | null {
const targetKey = this.getTargetIdentifier(target);
const metadataMap = this.storage.get(targetKey);
if (metadataMap instanceof Map) {
return metadataMap;
}
return null;
}
keys(): string[] {
return Array.from(this.storage.keys());
}

allList(): Map<string, Map<MetadataKey, MetadataValue>> {
const entries = this.storage.get('*', { pattern: true, withTuples: true }) as [string, Map<MetadataKey, MetadataValue>][];
const allMetadata = new Map<string, Map<MetadataKey, MetadataValue>>();

for (const [key, value] of entries) {
if (value instanceof Map) {
allMetadata.set(key, value);
}
}
return allMetadata;
}
}

export default ReflectStorage;
18 changes: 18 additions & 0 deletions packages/metadata/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": ".",
"outDir": "."
},
"files": [],
"include": ["**/*.ts"],
"exclude": ["node_modules"],
"references": [
{
"path": "../common"
},
{
"path": "../cache"
}
]
}
6 changes: 6 additions & 0 deletions packages/metadata/types/metadata.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type MetadataKey = string | symbol;
export type MetadataValue = unknown;
export type MetadataTarget = object | Function;
export type MetadataPropertyKey = string | symbol;
export type MetadataParameterIndex = number;
export type Decorator = ClassDecorator & PropertyDecorator & MethodDecorator & ParameterDecorator;
42 changes: 42 additions & 0 deletions packages/metadata/utils/metadta.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { isClass } from '@gland/common';
import { MetadataKey, MetadataParameterIndex, MetadataTarget } from '../types/metadata.types';
export const MetadataScope = {
CLASS: 'class',
METHOD: 'method',
PROPERTY: 'property',
PARAMETER: 'param',
} as const;
export type MetadataScopeType = keyof typeof MetadataScope;

/**
* Construct a fully qualified metadata key.
*/
export function constructKey(metadataKey: MetadataKey, propertyKey?: MetadataKey, parameterIndex?: MetadataParameterIndex): MetadataKey {
let key = String(metadataKey);
if (typeof parameterIndex === 'number') {
key = `${MetadataScope.PARAMETER}:${parameterIndex}:${key}`;
}
if (typeof propertyKey !== 'undefined') {
if (typeof parameterIndex === 'undefined') {
key = `${MetadataScope.METHOD}:${String(propertyKey)}:${key}`; // This is used for method metadata
} else {
key = `${MetadataScope.PROPERTY}:${String(propertyKey)}:${key}`; // Correct prefix for property metadata
}
}

if (typeof propertyKey === 'undefined' && !parameterIndex) {
key = `${MetadataScope.CLASS}:${key}`;
}
return key;
}
/**
* Resolves the target to its class constructor.
* If the target is a class, it is returned as-is.
* If the target is an instance, its constructor is returned.
*
* @param target - The target to resolve (class or instance).
* @returns The resolved class constructor.
*/
export function resolveTargetClass(target: MetadataTarget): MetadataTarget | null {
return isClass(target) ? target : target.constructor;
}

0 comments on commit 5f5feff

Please sign in to comment.