Skip to content

Commit

Permalink
feat(Metadata): Add the metadata package
Browse files Browse the repository at this point in the history
  • Loading branch information
a11delavar committed Jan 6, 2025
1 parent 4bf86e5 commit 6de6b5d
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 2 deletions.
21 changes: 21 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

71 changes: 71 additions & 0 deletions packages/Metadata/createMetadataDecorator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { createMetadataDecorator } from './createMetadataDecorator.js'
import { type } from './type.js'

const meta = createMetadataDecorator(Symbol('meta'))

describe('createMetadata', () => {
class TestClassZero { }
@meta('Test #1') class TestClassOne { }
class TestClassTwo extends TestClassOne { }
@meta('Test #3') class TestClassThree extends TestClassTwo { }

describe('on class', () => {
it('should return undefined for a class without the metadata', () => {
expect(meta.get(TestClassZero)).toBe(undefined)
})

it('should attach the metadata onto a class', () => {
expect(meta.get(TestClassOne)).toBe('Test #1')
})

it('should inherit the metadata from a parent class', () => {
expect(meta.get(TestClassTwo)).toBe('Test #1')
})

it('should override the metadata from a parent class', () => {
expect(meta.get(TestClassThree)).toBe('Test #3')
})
})

describe('on properties', () => {
class TestPropertyOne {
@meta('Some property') prop?: string
}

class TestPropertyTwo extends TestPropertyOne {
override prop = ''
}

class TestPropertyThree extends TestPropertyTwo {
@meta('Overridden property') override prop = ''
nested = new TestPropertyOne()
}

it('should attach the metadata onto a property', () => {
expect(meta.get(TestPropertyOne, 'prop')).toBe('Some property')
})

it('should inherit the metadata from a parent property', () => {
expect(meta.get(TestPropertyTwo, 'prop')).toBe('Some property')
})

it('should override the metadata from a parent property', () => {
expect(meta.get(TestPropertyThree, 'prop')).toBe('Overridden property')
})
})

describe('by key path', () => {
class TestKeyPathOne {
@meta('Some property') property?: string
}

class TestKeyPathTwo {
@type(TestKeyPathOne)
@meta('Property One') one?: TestKeyPathOne
}

it('should return the metadata by key path', () => {
expect(meta.getByKeyPath(TestKeyPathTwo, 'one.property')).toBe('Some property')
})
})
})
31 changes: 31 additions & 0 deletions packages/Metadata/createMetadataDecorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Creates a metadata decorator with the given key supporting both class and property metadata.
* It also provides a getter for retrieving metadata by key and key-path (e.g. 'property.subProperty').
* The key-path getter needs the property to be decorated with `@type(SubPropertyType)`
* for the generated decorator to be able to resolve the metadata.
*/
export function createMetadataDecorator(key: symbol) {
function metadata(value: unknown) {
return (target: any, propertyKey?: string) => {
Reflect.defineMetadata(key, value, target, propertyKey!)
}
}

metadata.get = function (constructor: Constructor<any>, propertyKey?: string) {
return propertyKey === undefined
? Reflect.getMetadata(key, constructor)
: Reflect.getMetadata(key, constructor.prototype, propertyKey)
}

metadata.getByKeyPath = function <T>(constructor: Constructor<T>, keyPath: KeyPathOf<T>) {
const keys = keyPath.split('.')
const key = keys.pop() as string
const parent = keys.reduce((previousType, key) => type.get(previousType, key), constructor)
if (!parent) {
throw new Error(`Could not resolve type for key path "${keyPath}". Ensure nested properties are decorated with @type(SubPropertyType)`)
}
return metadata.get(parent, key)
}

return metadata
}
9 changes: 9 additions & 0 deletions packages/Metadata/description.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createMetadataDecorator } from './createMetadataDecorator.js'

export const description = createMetadataDecorator(Symbol('description'))
globalThis.description = description

declare global {
// eslint-disable-next-line no-var
var description: typeof import('./description.js').description
}
10 changes: 10 additions & 0 deletions packages/Metadata/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'reflect-metadata'
import '@a11d/constructor'
import '@a11d/key-path'
export * from './createMetadataDecorator.js'
export * from './type.js'
import './type.js'
export * from './label.js'
import './label.js'
export * from './description.js'
import './description.js'
9 changes: 9 additions & 0 deletions packages/Metadata/label.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createMetadataDecorator } from './createMetadataDecorator.js'

export const label = createMetadataDecorator(Symbol('label'))
globalThis.label = label

declare global {
// eslint-disable-next-line no-var
var label: typeof import('./label.js').label
}
37 changes: 37 additions & 0 deletions packages/Metadata/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@a11d/metadata",
"version": "0.1.0",
"description": "A set of behavior-less metadata decorators.",
"repository": {
"type": "git",
"url": "git+https://github.com/a11delavar/lit-application.git"
},
"keywords": [
"metadata",
"decorator",
"attribute",
"reflect",
"reflect-metadata"
],
"author": "a11delavar",
"license": "MIT",
"bugs": {
"url": "https://github.com/a11delavar/lit-application/issues"
},
"homepage": "https://github.com/a11delavar/lit-application#readme",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc"
},
"dependencies": {
"tslib": "x",
"reflect-metadata": "0.x",
"@a11d/key-path": "1.x",
"@a11d/constructor": "x"
}
}
11 changes: 11 additions & 0 deletions packages/Metadata/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"exclude": [
"./dist",
"*.test.ts"
],
"compilerOptions": {
"baseUrl": ".",
"outDir": "./dist"
}
}
23 changes: 23 additions & 0 deletions packages/Metadata/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const key = 'design:type'

/**
* A general-purpose metadata decorator to store the type of a property in runtime.
*/
export function type<Target, TKey extends keyof Target>(type: Constructor<Target[TKey]>) {
return (target: Target, propertyKey?: TKey) => {
Reflect.defineMetadata(key, type, target as any, propertyKey as any)
}
}

type.get = function (constructor: Constructor<any>, propertyKey: string) {
return Reflect.getMetadata(key, constructor.prototype, propertyKey)
}

globalThis.type = type

declare global {
// eslint-disable-next-line no-var
var type: typeof import('./type.js').type & {
get: typeof import('./type.js').type.get
}
}
3 changes: 1 addition & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
"incremental": true,
"declaration": true,
"declarationMap": true,
"target": "ES2020",
"target": "ES2022",
"lib": [
"ES2021",
"DOM",
"DOM.Iterable"
],
Expand Down

0 comments on commit 6de6b5d

Please sign in to comment.