@apistudio/apim-cli
Version:
CLI for API Management Products
229 lines (210 loc) • 5.59 kB
text/typescript
/**
* Copyright IBM Corp. 2024, 2025
*/
import { z } from 'zod';
import {
BaseModel,
IfConditionSchema,
RequestSkippedSchema,
StopOnFailSchema,
} from './shared.schema.js';
import { EnvironmentSchema } from './environment.schema.js';
import { AssertionSchema } from './assertions.schema.js';
const refRule = z
.string()
.refine((val) => val !== '', {
message: '$ref cannot be an empty string',
})
.optional();
const apiRefRule = z
.union([
z.string().refine((val) => val !== '', {
message: '$ref cannot be an empty string',
}),
z.array(
z.string().refine((val) => val !== '', {
message: '$ref array items cannot be empty strings',
}),
),
])
.optional();
const RawPayloadSchema = z
.object({
json: z.string().optional(),
js: z.string().optional(),
html: z.string().optional(),
xml: z.string().optional(),
})
.strict()
.optional();
const KeyValueSchema = z
.array(
z.object({
key: z.string(),
value: z.any().refine((val) => val !== undefined, {
message: 'value is required',
}),
type: z.string().optional(),
}),
)
.optional();
export const PayloadUnionSchema = z
.object({
raw: RawPayloadSchema,
urlEncodedFormData: KeyValueSchema,
formData: KeyValueSchema,
})
.refine(
(data) => {
const keys = ['raw', 'urlEncodedFormData', 'formData'];
const presentKeys = keys.filter(
(key) => data[key as keyof typeof data] !== undefined,
);
return presentKeys.length === 1;
},
{
message:
'Exactly one of raw, urlEncodedFormData, or formData must be provided in payload',
},
);
const ApiRefOrEndpointSchema = z
.object({
$ref: apiRefRule,
$endpoint: z.string().optional(),
})
.refine(
(data) => (data.$ref && !data.$endpoint) || (!data.$ref && data.$endpoint),
{
message: 'Either $ref or $endpoint must be provided, but not both in api',
path: ['$ref', '$endpoint'],
},
);
const AssertionRefSchema = z
.object({
$ref: refRule,
assertions: z.array(AssertionSchema).optional(),
})
.refine(
(data) =>
data == undefined ||
data.$ref !== undefined ||
(data.assertions !== undefined && data.assertions.length > 0),
{
message:
'Either $ref or assertions (non-empty array) with complete data must be provided',
path: ['$ref', 'assertions'],
},
);
const EnvironmentSpecSchema = z
.object({
$ref: refRule,
variables: z.array(EnvironmentSchema).optional(),
})
.refine(
(data) =>
data == undefined ||
data.$ref !== undefined ||
(data.variables !== undefined && data.variables.length > 0),
{
message:
'Either $ref or variables (non-empty array) must be provided in environment',
path: ['$ref', 'variables'],
},
);
const BasicAuthSchema = z.object({
username: z.string(),
password: z.string(),
});
export const AuthSchema = z
.object({
noauth: z.boolean().optional(),
bearerToken: z.string().optional(),
basicAuth: BasicAuthSchema.optional(),
})
.refine(
(data) => {
const keys = ['noauth', 'bearerToken', 'basicAuth'];
const present = keys.filter(
(key) => data[key as keyof typeof data] !== undefined,
);
return present.length <= 1;
},
{
message:
'Only one of noauth, bearerToken, or basicAuth must be provided in auth',
path: ['auth'],
},
);
export const TestStepSchema = z.object({
endpoint: z.string().optional(),
method: z.string(),
if: IfConditionSchema.optional(),
stopOnFail: StopOnFailSchema.optional(),
skipped: RequestSkippedSchema.optional(),
resource: z.string(),
headers: z
.array(
z.object({
key: z.string(),
value: z.any().refine((val) => val !== undefined, {
message: 'value is required in headers',
}),
description: z.string().optional(),
}),
)
.optional(),
auth: AuthSchema.optional(),
payload: PayloadUnionSchema.optional(),
settings: z
.object({
sslVerification: z.boolean().optional(),
encodeURL: z.boolean().optional(),
})
.optional(),
parameters: KeyValueSchema.optional(),
assertions: z
.union([
// New format: array of objects with $ref
z.array(AssertionRefSchema.optional()),
// Single assertion with direct $ref property
AssertionRefSchema,
])
.optional(),
var: z
.union([
z.string(),
z.array(
z.union([
z.record(z.string(), z.string()),
z.object({
key: z.string(),
value: z.string(),
}),
]),
),
])
.optional(),
});
export const TestSchema = BaseModel.extend({
kind: z.literal('test'),
spec: z.object({
// will be the url to make the request
api: ApiRefOrEndpointSchema,
environment: z
.union([
// New format: array of objects with $ref
z.array(EnvironmentSpecSchema.optional()),
// Single assertion with direct $ref property
EnvironmentSpecSchema,
])
.optional(),
// tests which will have path and assertions
request: z.array(TestStepSchema),
}),
vcmId: z.string().optional(),
});
export type Test = z.infer<typeof TestSchema>;
export type Request = z.infer<typeof TestStepSchema>;
export type Payload = z.infer<typeof PayloadUnionSchema>;
export type AuthOptions = z.infer<typeof AuthSchema>;
export type Assertions = z.infer<typeof AssertionRefSchema>;