Skip to content

Commit bbab3b2

Browse files
author
Berend Sliedrecht
committed
feat: use zod for transformation and validation
Signed-off-by: Berend Sliedrecht <sliedrecht@berend.io>
1 parent d62d1d1 commit bbab3b2

18 files changed

+243
-330
lines changed

packages/action-menu/package.json

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
"main": "build/index",
44
"types": "build/index",
55
"version": "0.4.2",
6-
"files": [
7-
"build"
8-
],
6+
"files": ["build"],
97
"license": "Apache-2.0",
108
"publishConfig": {
119
"access": "public"
@@ -25,12 +23,10 @@
2523
},
2624
"dependencies": {
2725
"@credo-ts/core": "0.4.2",
28-
"class-transformer": "0.5.1",
29-
"class-validator": "0.14.1",
30-
"rxjs": "^7.2.0"
26+
"zod": "^3.22.4"
3127
},
3228
"devDependencies": {
33-
"reflect-metadata": "^0.1.13",
29+
"rxjs": "^7.2.0",
3430
"rimraf": "^4.4.0",
3531
"typescript": "~4.9.5"
3632
}

packages/action-menu/src/ActionMenuEvents.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ActionMenuState } from './ActionMenuState'
2-
import type { ActionMenuRecord } from './repository'
2+
import type ActionMenuRecord from './repository'
33
import type { BaseEvent } from '@credo-ts/core'
44

55
/**
Lines changed: 33 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,43 @@
1-
import type { ActionMenuOptionOptions } from '../models'
2-
3-
import { AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core'
4-
import { Expose, Type } from 'class-transformer'
5-
import { IsInstance, IsOptional, IsString } from 'class-validator'
1+
import { AgentMessage, parseMessageType, utils } from '@credo-ts/core'
2+
import { z } from 'zod'
63

74
import { ActionMenuOption } from '../models'
5+
import { actionMenuOptionSchema } from '../models/ActionMenuOption'
6+
import { arrIntoCls } from '../models/ActionMenuOptionForm'
7+
8+
const menuMessageSchema = z
9+
.object({
10+
id: z.string().default(utils.uuid()),
11+
title: z.string(),
12+
description: z.string(),
13+
errormsg: z.string().optional(),
14+
options: z.array(actionMenuOptionSchema).transform(arrIntoCls<ActionMenuOption>(ActionMenuOption)),
15+
threadId: z.string().optional(),
16+
})
17+
.transform((o) => ({
18+
...o,
19+
errorMessage: o.errormsg,
20+
}))
21+
22+
export type MenuMessageOptions = z.input<typeof menuMessageSchema>
823

9-
/**
10-
* @internal
11-
*/
12-
export interface MenuMessageOptions {
13-
id?: string
14-
title: string
15-
description: string
16-
errorMessage?: string
17-
options: ActionMenuOptionOptions[]
18-
threadId?: string
19-
}
20-
21-
/**
22-
* @internal
23-
*/
2424
export class MenuMessage extends AgentMessage {
25-
public constructor(options: MenuMessageOptions) {
26-
super()
27-
28-
if (options) {
29-
this.id = options.id ?? this.generateId()
30-
this.title = options.title
31-
this.description = options.description
32-
this.errorMessage = options.errorMessage
33-
this.options = options.options.map((p) => new ActionMenuOption(p))
34-
if (options.threadId) {
35-
this.setThread({
36-
threadId: options.threadId,
37-
})
38-
}
39-
}
40-
}
41-
42-
@IsValidMessageType(MenuMessage.type)
4325
public readonly type = MenuMessage.type.messageTypeUri
4426
public static readonly type = parseMessageType('https://didcomm.org/action-menu/1.0/menu')
4527

46-
@IsString()
47-
public title!: string
48-
49-
@IsString()
50-
public description!: string
51-
52-
@Expose({ name: 'errormsg' })
53-
@IsString()
54-
@IsOptional()
28+
public title: string
29+
public description: string
5530
public errorMessage?: string
31+
public options: Array<ActionMenuOption>
5632

57-
@IsInstance(ActionMenuOption, { each: true })
58-
@Type(() => ActionMenuOption)
59-
public options!: ActionMenuOption[]
33+
public constructor(options: MenuMessageOptions) {
34+
super()
35+
36+
const parsedOptions = menuMessageSchema.parse(options)
37+
this.id = parsedOptions.id
38+
this.title = parsedOptions.title
39+
this.description = parsedOptions.description
40+
this.errorMessage = parsedOptions.errorMessage
41+
this.options = parsedOptions.options
42+
}
6043
}
Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,33 @@
1-
import { AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core'
2-
import { IsOptional, IsString } from 'class-validator'
1+
import { AgentMessage, parseMessageType, utils } from '@credo-ts/core'
2+
import { z } from 'zod'
33

4-
/**
5-
* @internal
6-
*/
7-
export interface PerformMessageOptions {
8-
id?: string
9-
name: string
10-
params?: Record<string, string>
11-
threadId: string
12-
}
4+
const performMessageSchema = z.object({
5+
id: z.string().default(utils.uuid()),
136

14-
/**
15-
* @internal
16-
*/
17-
export class PerformMessage extends AgentMessage {
18-
public constructor(options: PerformMessageOptions) {
19-
super()
7+
// TODO(zod): validate the name like is done in `@IsValidMessageType(PerformMessage.type)`
8+
name: z.string(),
9+
params: z.record(z.string()).optional(),
10+
threadId: z.string(),
11+
})
2012

21-
if (options) {
22-
this.id = options.id ?? this.generateId()
23-
this.name = options.name
24-
this.params = options.params
25-
this.setThread({
26-
threadId: options.threadId,
27-
})
28-
}
29-
}
13+
export type PerformMessageOptions = z.input<typeof performMessageSchema>
3014

31-
@IsValidMessageType(PerformMessage.type)
15+
export class PerformMessage extends AgentMessage {
3216
public readonly type = PerformMessage.type.messageTypeUri
3317
public static readonly type = parseMessageType('https://didcomm.org/action-menu/1.0/perform')
3418

35-
@IsString()
36-
public name!: string
37-
38-
@IsString({ each: true })
39-
@IsOptional()
19+
public name: string
4020
public params?: Record<string, string>
21+
22+
public constructor(options: PerformMessageOptions) {
23+
super()
24+
25+
const parsedOptions = performMessageSchema.parse(options)
26+
this.id = parsedOptions.id
27+
this.name = parsedOptions.name
28+
this.params = parsedOptions.params
29+
this.setThread({
30+
threadId: parsedOptions.threadId,
31+
})
32+
}
4133
}
Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,25 @@
1-
import type { ActionMenuOptionOptions } from './ActionMenuOption'
1+
import { z } from 'zod'
22

3-
import { Type } from 'class-transformer'
4-
import { IsInstance, IsString } from 'class-validator'
3+
import { ActionMenuOption, actionMenuOptionSchema } from './ActionMenuOption'
4+
import { arrIntoCls } from './ActionMenuOptionForm'
55

6-
import { ActionMenuOption } from './ActionMenuOption'
6+
export const actionMenuSchema = z.object({
7+
title: z.string(),
8+
description: z.string(),
9+
options: z.array(actionMenuOptionSchema).transform(arrIntoCls<ActionMenuOption>(ActionMenuOption)),
10+
})
711

8-
/**
9-
* @public
10-
*/
11-
export interface ActionMenuOptions {
12-
title: string
13-
description: string
14-
options: ActionMenuOptionOptions[]
15-
}
12+
export type ActionMenuOptions = z.input<typeof actionMenuSchema>
1613

17-
/**
18-
* @public
19-
*/
2014
export class ActionMenu {
15+
public title: string
16+
public description: string
17+
public options: Array<ActionMenuOption>
18+
2119
public constructor(options: ActionMenuOptions) {
22-
if (options) {
23-
this.title = options.title
24-
this.description = options.description
25-
this.options = options.options.map((p) => new ActionMenuOption(p))
26-
}
20+
const parsedOptions = actionMenuSchema.parse(options)
21+
this.title = parsedOptions.title
22+
this.description = parsedOptions.description
23+
this.options = parsedOptions.options
2724
}
28-
29-
@IsString()
30-
public title!: string
31-
32-
@IsString()
33-
public description!: string
34-
35-
@IsInstance(ActionMenuOption, { each: true })
36-
@Type(() => ActionMenuOption)
37-
public options!: ActionMenuOption[]
3825
}
Lines changed: 22 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,30 @@
1-
import type { ActionMenuFormOptions } from './ActionMenuOptionForm'
1+
import { z } from 'zod'
22

3-
import { Type } from 'class-transformer'
4-
import { IsBoolean, IsInstance, IsOptional, IsString } from 'class-validator'
3+
import { ActionMenuForm, actionMenuFormSchema, intoOptCls } from './ActionMenuOptionForm'
54

6-
import { ActionMenuForm } from './ActionMenuOptionForm'
5+
export const actionMenuOptionSchema = z.object({
6+
name: z.string(),
7+
title: z.string(),
8+
description: z.string(),
9+
disabled: z.boolean().optional(),
10+
form: actionMenuFormSchema.optional().transform(intoOptCls<ActionMenuForm>(ActionMenuForm)),
11+
})
712

8-
/**
9-
* @public
10-
*/
11-
export interface ActionMenuOptionOptions {
12-
name: string
13-
title: string
14-
description: string
15-
disabled?: boolean
16-
form?: ActionMenuFormOptions
17-
}
13+
export type ActionMenuOptionOptions = z.input<typeof actionMenuOptionSchema>
1814

19-
/**
20-
* @public
21-
*/
2215
export class ActionMenuOption {
23-
public constructor(options: ActionMenuOptionOptions) {
24-
if (options) {
25-
this.name = options.name
26-
this.title = options.title
27-
this.description = options.description
28-
this.disabled = options.disabled
29-
if (options.form) {
30-
this.form = new ActionMenuForm(options.form)
31-
}
32-
}
33-
}
34-
35-
@IsString()
36-
public name!: string
37-
38-
@IsString()
39-
public title!: string
40-
41-
@IsString()
42-
public description!: string
43-
44-
@IsBoolean()
45-
@IsOptional()
16+
public name: string
17+
public title: string
18+
public description: string
4619
public disabled?: boolean
47-
48-
@IsInstance(ActionMenuForm)
49-
@Type(() => ActionMenuForm)
50-
@IsOptional()
5120
public form?: ActionMenuForm
21+
22+
public constructor(options: ActionMenuOptionOptions) {
23+
const parsedOptions = actionMenuOptionSchema.parse(options)
24+
this.name = parsedOptions.name
25+
this.title = parsedOptions.title
26+
this.description = parsedOptions.description
27+
this.disabled = parsedOptions.disabled
28+
this.form = parsedOptions.form
29+
}
5230
}
Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,39 @@
1-
import type { ActionMenuFormParameterOptions } from './ActionMenuOptionFormParameter'
1+
import { z } from 'zod'
22

3-
import { Expose, Type } from 'class-transformer'
4-
import { IsInstance, IsString } from 'class-validator'
3+
import { ActionMenuFormParameter, actionMenuFormParameterSchema } from './ActionMenuOptionFormParameter'
54

6-
import { ActionMenuFormParameter } from './ActionMenuOptionFormParameter'
5+
// TODO(zod): these should not have any ts-expect-error and should return the type based on `Cls` input, not the generic
6+
export const intoCls =
7+
<T>(Cls: unknown) =>
8+
(i: unknown): T =>
9+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
10+
// @ts-expect-error
11+
new Cls(i)
12+
export const arrIntoCls =
13+
<T>(Cls: unknown) =>
14+
(o: Array<unknown>): Array<T> =>
15+
o.map(intoCls(Cls))
16+
export const intoOptCls =
17+
<T>(Cls: unknown) =>
18+
(i?: unknown): undefined | T =>
19+
i ? intoCls<T>(Cls)(i) : undefined
720

8-
/**
9-
* @public
10-
*/
11-
export interface ActionMenuFormOptions {
12-
description: string
13-
params: ActionMenuFormParameterOptions[]
14-
submitLabel: string
15-
}
21+
export const actionMenuFormSchema = z.object({
22+
description: z.string(),
23+
params: z
24+
.array(actionMenuFormParameterSchema)
25+
.transform(arrIntoCls<ActionMenuFormParameter>(ActionMenuFormParameter)),
26+
})
27+
28+
export type ActionMenuFormOptions = z.input<typeof actionMenuFormSchema>
1629

17-
/**
18-
* @public
19-
*/
2030
export class ActionMenuForm {
31+
public description: string
32+
public params: Array<ActionMenuFormParameter>
33+
2134
public constructor(options: ActionMenuFormOptions) {
22-
if (options) {
23-
this.description = options.description
24-
this.params = options.params.map((p) => new ActionMenuFormParameter(p))
25-
this.submitLabel = options.submitLabel
26-
}
35+
const parsedOptions = actionMenuFormSchema.parse(options)
36+
this.description = parsedOptions.description
37+
this.params = parsedOptions.params
2738
}
28-
29-
@IsString()
30-
public description!: string
31-
32-
@Expose({ name: 'submit-label' })
33-
@IsString()
34-
public submitLabel!: string
35-
36-
@IsInstance(ActionMenuFormParameter, { each: true })
37-
@Type(() => ActionMenuFormParameter)
38-
public params!: ActionMenuFormParameter[]
3939
}

0 commit comments

Comments
 (0)