Skip to content

Commit

Permalink
feat: use zod for transformation and validation
Browse files Browse the repository at this point in the history
Signed-off-by: Berend Sliedrecht <[email protected]>
  • Loading branch information
Berend Sliedrecht committed Feb 26, 2024
1 parent d7c2bbb commit 1adc7d8
Show file tree
Hide file tree
Showing 18 changed files with 242 additions and 329 deletions.
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

0 comments on commit 1adc7d8

Please sign in to comment.