-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #26 from medishen/dev/v2
feat(metadata): Add metadata package implementation with core functionality and utility support
- Loading branch information
Showing
7 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |