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

feat: use zod for transformation and validation #1777

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
10 changes: 3 additions & 7 deletions packages/action-menu/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
"main": "build/index",
"types": "build/index",
"version": "0.4.2",
"files": [
"build"
],
"files": ["build"],
"license": "Apache-2.0",
"publishConfig": {
"access": "public"
Expand All @@ -25,12 +23,10 @@
},
"dependencies": {
"@credo-ts/core": "0.4.2",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"rxjs": "^7.2.0"
"zod": "^3.22.4"
},
"devDependencies": {
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0",
"rimraf": "^4.4.0",
"typescript": "~4.9.5"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/action-menu/src/ActionMenuEvents.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ActionMenuState } from './ActionMenuState'
import type { ActionMenuRecord } from './repository'
import type ActionMenuRecord from './repository'

Check failure on line 2 in packages/action-menu/src/ActionMenuEvents.ts

View workflow job for this annotation

GitHub Actions / Validate

No default export found in imported module "./repository"
import type { BaseEvent } from '@credo-ts/core'

/**
Expand Down
83 changes: 33 additions & 50 deletions packages/action-menu/src/messages/MenuMessage.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,43 @@
import type { ActionMenuOptionOptions } from '../models'

import { AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core'
import { Expose, Type } from 'class-transformer'
import { IsInstance, IsOptional, IsString } from 'class-validator'
import { AgentMessage, parseMessageType, utils } from '@credo-ts/core'
import { z } from 'zod'

import { ActionMenuOption } from '../models'
import { actionMenuOptionSchema } from '../models/ActionMenuOption'
import { arrIntoCls } from '../models/ActionMenuOptionForm'

const menuMessageSchema = z
.object({
id: z.string().default(utils.uuid()),
title: z.string(),
description: z.string(),
errormsg: z.string().optional(),
options: z.array(actionMenuOptionSchema).transform(arrIntoCls<ActionMenuOption>(ActionMenuOption)),
threadId: z.string().optional(),
})
.transform((o) => ({
...o,
errorMessage: o.errormsg,
}))

export type MenuMessageOptions = z.input<typeof menuMessageSchema>

/**
* @internal
*/
export interface MenuMessageOptions {
id?: string
title: string
description: string
errorMessage?: string
options: ActionMenuOptionOptions[]
threadId?: string
}

/**
* @internal
*/
export class MenuMessage extends AgentMessage {
public constructor(options: MenuMessageOptions) {
super()

if (options) {
this.id = options.id ?? this.generateId()
this.title = options.title
this.description = options.description
this.errorMessage = options.errorMessage
this.options = options.options.map((p) => new ActionMenuOption(p))
if (options.threadId) {
this.setThread({
threadId: options.threadId,
})
}
}
}

@IsValidMessageType(MenuMessage.type)
public readonly type = MenuMessage.type.messageTypeUri
public static readonly type = parseMessageType('https://didcomm.org/action-menu/1.0/menu')

@IsString()
public title!: string

@IsString()
public description!: string

@Expose({ name: 'errormsg' })
@IsString()
@IsOptional()
public title: string
public description: string
public errorMessage?: string
public options: Array<ActionMenuOption>

@IsInstance(ActionMenuOption, { each: true })
@Type(() => ActionMenuOption)
public options!: ActionMenuOption[]
public constructor(options: MenuMessageOptions) {
super()

const parsedOptions = menuMessageSchema.parse(options)
this.id = parsedOptions.id
this.title = parsedOptions.title
this.description = parsedOptions.description
this.errorMessage = parsedOptions.errorMessage
this.options = parsedOptions.options
}
}
56 changes: 24 additions & 32 deletions packages/action-menu/src/messages/PerformMessage.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,33 @@
import { AgentMessage, IsValidMessageType, parseMessageType } from '@credo-ts/core'
import { IsOptional, IsString } from 'class-validator'
import { AgentMessage, parseMessageType, utils } from '@credo-ts/core'
import { z } from 'zod'

/**
* @internal
*/
export interface PerformMessageOptions {
id?: string
name: string
params?: Record<string, string>
threadId: string
}
const performMessageSchema = z.object({
id: z.string().default(utils.uuid()),

/**
* @internal
*/
export class PerformMessage extends AgentMessage {
public constructor(options: PerformMessageOptions) {
super()
// TODO(zod): validate the name like is done in `@IsValidMessageType(PerformMessage.type)`
name: z.string(),
params: z.record(z.string()).optional(),
threadId: z.string(),
})

if (options) {
this.id = options.id ?? this.generateId()
this.name = options.name
this.params = options.params
this.setThread({
threadId: options.threadId,
})
}
}
export type PerformMessageOptions = z.input<typeof performMessageSchema>

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

@IsString()
public name!: string

@IsString({ each: true })
@IsOptional()
public name: string
public params?: Record<string, string>

public constructor(options: PerformMessageOptions) {
super()

const parsedOptions = performMessageSchema.parse(options)
this.id = parsedOptions.id
this.name = parsedOptions.name
this.params = parsedOptions.params
this.setThread({
threadId: parsedOptions.threadId,
})
}
}
47 changes: 17 additions & 30 deletions packages/action-menu/src/models/ActionMenu.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,25 @@
import type { ActionMenuOptionOptions } from './ActionMenuOption'
import { z } from 'zod'

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

import { ActionMenuOption } from './ActionMenuOption'
export const actionMenuSchema = z.object({
title: z.string(),
description: z.string(),
options: z.array(actionMenuOptionSchema).transform(arrIntoCls<ActionMenuOption>(ActionMenuOption)),
})

/**
* @public
*/
export interface ActionMenuOptions {
title: string
description: string
options: ActionMenuOptionOptions[]
}
export type ActionMenuOptions = z.input<typeof actionMenuSchema>

/**
* @public
*/
export class ActionMenu {
public title: string
public description: string
public options: Array<ActionMenuOption>

public constructor(options: ActionMenuOptions) {
if (options) {
this.title = options.title
this.description = options.description
this.options = options.options.map((p) => new ActionMenuOption(p))
}
const parsedOptions = actionMenuSchema.parse(options)
this.title = parsedOptions.title
this.description = parsedOptions.description
this.options = parsedOptions.options
}

@IsString()
public title!: string

@IsString()
public description!: string

@IsInstance(ActionMenuOption, { each: true })
@Type(() => ActionMenuOption)
public options!: ActionMenuOption[]
}
66 changes: 22 additions & 44 deletions packages/action-menu/src/models/ActionMenuOption.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,30 @@
import type { ActionMenuFormOptions } from './ActionMenuOptionForm'
import { z } from 'zod'

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

import { ActionMenuForm } from './ActionMenuOptionForm'
export const actionMenuOptionSchema = z.object({
name: z.string(),
title: z.string(),
description: z.string(),
disabled: z.boolean().optional(),
form: actionMenuFormSchema.optional().transform(intoOptCls<ActionMenuForm>(ActionMenuForm)),
})

/**
* @public
*/
export interface ActionMenuOptionOptions {
name: string
title: string
description: string
disabled?: boolean
form?: ActionMenuFormOptions
}
export type ActionMenuOptionOptions = z.input<typeof actionMenuOptionSchema>

/**
* @public
*/
export class ActionMenuOption {
public constructor(options: ActionMenuOptionOptions) {
if (options) {
this.name = options.name
this.title = options.title
this.description = options.description
this.disabled = options.disabled
if (options.form) {
this.form = new ActionMenuForm(options.form)
}
}
}

@IsString()
public name!: string

@IsString()
public title!: string

@IsString()
public description!: string

@IsBoolean()
@IsOptional()
public name: string
public title: string
public description: string
public disabled?: boolean

@IsInstance(ActionMenuForm)
@Type(() => ActionMenuForm)
@IsOptional()
public form?: ActionMenuForm

public constructor(options: ActionMenuOptionOptions) {
const parsedOptions = actionMenuOptionSchema.parse(options)
this.name = parsedOptions.name
this.title = parsedOptions.title
this.description = parsedOptions.description
this.disabled = parsedOptions.disabled
this.form = parsedOptions.form
}
}
62 changes: 31 additions & 31 deletions packages/action-menu/src/models/ActionMenuOptionForm.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,39 @@
import type { ActionMenuFormParameterOptions } from './ActionMenuOptionFormParameter'
import { z } from 'zod'

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

import { ActionMenuFormParameter } from './ActionMenuOptionFormParameter'
// TODO(zod): these should not have any ts-expect-error and should return the type based on `Cls` input, not the generic
export const intoCls =
<T>(Cls: unknown) =>
(i: unknown): T =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
new Cls(i)
export const arrIntoCls =
<T>(Cls: unknown) =>
(o: Array<unknown>): Array<T> =>
o.map(intoCls(Cls))
export const intoOptCls =
<T>(Cls: unknown) =>
(i?: unknown): undefined | T =>
i ? intoCls<T>(Cls)(i) : undefined

/**
* @public
*/
export interface ActionMenuFormOptions {
description: string
params: ActionMenuFormParameterOptions[]
submitLabel: string
}
export const actionMenuFormSchema = z.object({
description: z.string(),
params: z
.array(actionMenuFormParameterSchema)
.transform(arrIntoCls<ActionMenuFormParameter>(ActionMenuFormParameter)),
})

export type ActionMenuFormOptions = z.input<typeof actionMenuFormSchema>

/**
* @public
*/
export class ActionMenuForm {
public description: string
public params: Array<ActionMenuFormParameter>

public constructor(options: ActionMenuFormOptions) {
if (options) {
this.description = options.description
this.params = options.params.map((p) => new ActionMenuFormParameter(p))
this.submitLabel = options.submitLabel
}
const parsedOptions = actionMenuFormSchema.parse(options)
this.description = parsedOptions.description
this.params = parsedOptions.params
}

@IsString()
public description!: string

@Expose({ name: 'submit-label' })
@IsString()
public submitLabel!: string

@IsInstance(ActionMenuFormParameter, { each: true })
@Type(() => ActionMenuFormParameter)
public params!: ActionMenuFormParameter[]
}
Loading
Loading