Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update version #10

Merged
merged 4 commits into from
Mar 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 47 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,39 @@ A simple and versatile mapping utility for Typescript.
## Get Started

```bash
npm i --save @kitbag/mapper
# bun
bun add @kitbag/mapper
# yarn
yarn add @kitbag/mapper
# npm
npm install @kitbag/mapper
```

## Basic Setup

Each mapper relies an an underlying set of `Profile` objects to define what types are supported and how to map between them. Mappers are created with `createMapper` function, which takes `Profile[]` as it's only argument.

```ts
import { createMapper } from '@stackoverfloweth/mapper'
import mapper from '@kitbag/mapper'

const mapper = createMapper(profiles)
mapper.register(profiles)

mapper.map('source-key', source, 'destination-key')
```

### Profiles
The mapper relies an an underlying set of `Profile` objects to define what types are supported and how to map between them. To add profiles to the mapper, use `register`.

### Type Safety

In order to have type safety when using the router, you must register your profiles within the `Register` interface under the namespace `@kitbag/mapper`.

```ts
declare module '@kitbag/mapper' {
interface Register {
profiles: typeof profiles
}
}
```

## Profiles

Each profile defines a value for `sourceKey` and `destinationKey`. These keys must extend `string` and should be unique, beyond that the choice is irrelevant to the function of the mapper.

Expand All @@ -44,42 +61,52 @@ export const numberToDate = {
} as const satisfies Profile
```

The mapper will use the keys you define to provide type safety when calling map.
Note the [satisfies operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator) requires Typescript v4.9+.

Assuming you declared your own `Register` interface from [Type Safety](#type-safety). The mapper will use the keys you define to provide type safety when calling map.

```ts
mapper.map('number', 123, 'string') // "123"
mapper.map('number', 123, 'Date') // Wed Dec 31 1969...
mapper.map('number', 123, 'potato') // ERROR TS:2345 Argument of type '"potato"' is not assignable to parameter of type '"string" | "Date"'
```

Anytime `mapper.map` is called with source and/or destination keys that are not registered by a profile, it will throw the following error.
### ProfileNotFoundError

> 'Mapping profile not found'
Anytime `mapper.map` is called with source and/or destination keys that are not registered by a profile, it will throw `ProfileNotFoundError`.

### Loading profiles automatically
## Loading profiles automatically

This library provides a useful method for automatically loading profiles. If you store all of your profiles in the same folder with a simple barrel file.
This library provides a useful method for automatically loading profiles. If you store all of your profiles in the same folder with a simple [barrel file](https://github.com/basarat/typescript-book/blob/master/docs/tips/barrel.md).

```text
└── src
├── models
└── maps
├── primitives.ts
├── foo.ts
├── bar.ts
└── index.ts
├── index.ts
└── primitives
├── string.ts
├── number.ts
├── boolean.ts
└── index.ts
```

```ts
import { createMapper, loadProfiles } from '@stackoverfloweth/mapper'
import mapper, { loadProfiles } from '@kitbag/mapper'
import * as maybeProfiles from '@/maps'

const profiles = loadProfiles(maybeProfiles)

const mapper = createMapper(profiles)
mapper.register(profiles)
```

### Mapping an array
### ProfileTypeError

With most use cases involving an import that is not type safe, it's not unreasonable to assume something that doesn't satisfy `Profile` will get passed in. If `loadProfiles` is provided with anything that doesn't satisfy `Profile`, it will throw `ProfileTypeError`.

## Mapping an array

Because `TSource` and `TDestination` are not constrained, you can always define a profile that expects an array.

Expand All @@ -105,7 +132,7 @@ or the mapper provides a simpler method `mapMany`, which takes an array of `TSou
const mapped = mapper.mapMany('source-key', sources, 'destination-key')
```

### Nesting profiles
## Nesting profiles

Sometimes your business logic for mapping from `TSource` to `TDestination` might benefit from nesting profiles inside of other profiles. For example, if you have the following models

Expand Down Expand Up @@ -173,13 +200,11 @@ export const orderResponseToOrder = {

What you chose to name the profile doesn't matter to the mapper. In these examples I used the pattern `${sourceKey}To${destinationKey}` but this key is not currently used by `loadProfiles()` in any way.

### Implicit `any` TS error

If you're seeing the following error within your profile or within the file that calls `createMapper`
### Missing types or source type `never`

> '...' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
If you're seeing map as `(sourceKey: string, source: unknown, destinationKey: string) => unknown`, this likely means you missed setting the `Register` interface. See more about [type safety](#type-safety).

this is likely because you're missing the type annotations on your `map` method within a profile.
This could also be the result of your profiles not using `as const satisfies Profile`.

```ts
export const numberToString = {
Expand All @@ -190,15 +215,3 @@ export const numberToString = {
},
} as const satisfies Profile
```

adding `number` type for `source` and return type of `string` resolves the error.

```ts
export const numberToString = {
sourceKey: 'number',
destinationKey: 'string',
map: (source: number): string => {
return source.toString()
},
} as const satisfies Profile
```
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kitbag/mapper",
"version": "0.0.1",
"version": "0.0.3",
"description": "A simple and versatile mapping utility for Typescript.",
"author": "Evan Sutherland",
"bugs": {
Expand Down
21 changes: 20 additions & 1 deletion src/createMapper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Mapper, Profile, ProfileKey, RegisteredProfile, RegisteredProfiles, ProfileNotFoundError } from '@/types'
import { asArray } from '@/utilities'

export function createMapper(): Mapper<RegisteredProfiles> {

Expand All @@ -15,12 +16,28 @@ export function createMapper(): Mapper<RegisteredProfiles> {
return profile
}

const register: Mapper<RegisteredProfiles>['register'] = (profiles) => {
const register: Mapper<RegisteredProfiles>['register'] = (maybeProfiles) => {
const profiles = asArray(maybeProfiles)

for (const profile of profiles) {
profileMap.set(`${profile.sourceKey}-${profile.destinationKey}`, profile)
}
}

const has: Mapper<RegisteredProfiles>['has'] = (sourceKey: string, destinationKey: string) => {
try {
getProfile(sourceKey, destinationKey)

return true
} catch {
return false
}
}

const clear: Mapper<RegisteredProfiles>['clear'] = () => {
profileMap.clear()
}

const map: Mapper<RegisteredProfiles>['map'] = (sourceKey, source, destinationKey) => {
const profile = getProfile(sourceKey, destinationKey)

Expand All @@ -35,6 +52,8 @@ export function createMapper(): Mapper<RegisteredProfiles> {

const mapper: Mapper<RegisteredProfiles> = {
register,
has,
clear,
map,
mapMany,
}
Expand Down
12 changes: 12 additions & 0 deletions src/createmapper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,16 @@ test('mapMany returns the correct value', () => {
const value = mapper.mapMany('string', ['true'], 'boolean')

expect(value).toMatchObject([true])
})

test('register works with single profile', () => {
const singleProfile = {
sourceKey: 'foo',
destinationKey: 'bar',
map: (source: string): boolean => Boolean(source),
} as const satisfies Profile

mapper.register(singleProfile)

expect(mapper.has('foo', 'bar')).toBe(true)
})
2 changes: 1 addition & 1 deletion src/loadProfiles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AnyFunction } from '@/types'
import { ProfileTypeError } from '@/types/profileTypeError'
import { AnyFunction } from '@/utilities'

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Imported = Record<PropertyKey, any>
Expand Down
2 changes: 0 additions & 2 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export * from './function'
export * from './mapper'
export * from './profile'
export * from './profileNotFoundError'
export * from './profileTypeError'
export * from './promise'
export * from './register'
4 changes: 3 additions & 1 deletion src/types/mapper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ExtractSourceKeys, ExtractSources, ExtractDestinationKeys, ExtractDestinations, Profile } from '@/types/profile'

export type Mapper<TProfiles extends Readonly<Profile[]>, TProfile = TProfiles[number]> = {
register: (profiles: Profile[] | Readonly<Profile[]>) => void,
register: (profiles: Profile[] | Readonly<Profile[]> | Profile) => void,
clear: () => void,
has: (sourceKey: string, destinationKey: string) => boolean,
map: <
TSourceKey extends ExtractSourceKeys<TProfile>,
TSource extends ExtractSources<TProfile, TSourceKey>,
Expand Down
7 changes: 7 additions & 0 deletions src/utilities/array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function asArray<T>(value: T | T[] | Readonly<T[]>): T[] {
if (Array.isArray(value)) {
return value
}

return [value as T]
}
File renamed without changes.
3 changes: 3 additions & 0 deletions src/utilities/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './array'
export * from './function'
export * from './promise'
File renamed without changes.
Loading