-
-
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.
feat(metadata): Add metadata package implementation with core functio…
…nality and utility support This commit introduces the initial implementation of the metadata package which includes core functionality for managing metadata for classes methods properties and parameters The core component is the Reflector class which provides methods for defining retrieving checking and deleting metadata The package supports various metadata scopes such as class method property and parameter with functionality for each scope Key additions in this commit include the following components 1 The Reflector class which acts as the main entry point and exposes methods like defineMetadata getMetadata hasMetadata deleteMetadata and clearMetadata to interact with metadata storage 2 The metadata decorator factory that allows developers to define metadata for classes properties methods and parameters using a convenient decorator syntax 3 MetadataStorage interface that defines the methods for interacting with metadata storage including get set has delete clear and keys methods 4 ReflectStorage class which provides an in-memory storage solution for metadata using a Least Recently Used LRU cache policy implemented with MemoryCacheStore from the cache package 5 Utility functions in utils metadta utils including constructKey for generating fully qualified metadata keys and resolveTargetClass to resolve a target to its class constructor 6 Type definitions and interfaces in types metadata types which define the structure of metadata keys values and the supported targets as well as the decorator type definition 7 The metadata interface providing the contract for the MetadataStorage implementation 8 TypeScript configuration files including tsconfig json and package json to manage TypeScript compilation and package dependencies Additionally the metadata package includes references to the common and cache packages to leverage shared utilities and caching mechanisms The package supports storing metadata for various targets and provides functionality to list retrieve and remove metadata associated with these targets making it a powerful tool for metadata management in TypeScript applications
- Loading branch information
Showing
5 changed files
with
298 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,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,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; | ||
} |