Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// This file is auto-generated by @hey-api/openapi-ts

import * as v from 'valibot';

export const vChild = v.object({
id: v.optional(v.number())
});

export const vParent = v.object({
id: v.number(),
children: v.optional(v.array(vChild))
});
7 changes: 7 additions & 0 deletions packages/openapi-ts-tests/valibot/v1/test/3.1.x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ describe(`OpenAPI ${version}`, () => {
}),
description: 'validator schemas with BigInt and min/max constraints',
},
{
config: createConfig({
input: 'validators-acyclic-ref-order.yaml',
output: 'validators-acyclic-ref-order',
}),
description: 'orders acyclic validator schema references before dependents',
},
{
config: createConfig({
input: 'validators-circular-ref.json',
Expand Down
65 changes: 62 additions & 3 deletions packages/openapi-ts/src/plugins/valibot/v1/processor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { ref } from '@hey-api/codegen-core';
import type { SchemaExtractor } from '@hey-api/shared';
import { createSchemaProcessor, createSchemaWalker, pathToJsonPointer } from '@hey-api/shared';
import type { IR, SchemaExtractor } from '@hey-api/shared';
import {
createSchemaProcessor,
createSchemaWalker,
jsonPointerToPath,
normalizeJsonPointer,
pathToJsonPointer,
} from '@hey-api/shared';

import { exportAst } from '../shared/export';
import type { ProcessorContext, ProcessorResult } from '../shared/processor';
Expand All @@ -13,6 +19,54 @@ export function createProcessor(plugin: ValibotPlugin['Instance']): ProcessorRes

const extractorHooks = plugin.getHooks((hooks) => hooks.schemas?.shouldExtract);

function isCyclicReference(currentPointer: string, targetPointer: string): boolean {
if (targetPointer === currentPointer) {
return true;
}

return (
plugin.context.graph?.transitiveDependencies.get(targetPointer)?.has(currentPointer) ?? false
);
}

function ensureReferenceRegistered(currentPointer: string, $ref: string): void {
const targetPointer = normalizeJsonPointer($ref);
const query = {
artifact: 'valibot',
category: 'schema',
resource: 'definition',
resourceId: targetPointer,
};

if (plugin.isSymbolRegistered(query)) {
return;
}

if (isCyclicReference(currentPointer, targetPointer)) {
return;
}

const targetSchema = plugin.context.resolveIrRef<IR.SchemaObject>(targetPointer);
const targetPath = jsonPointerToPath(targetPointer);
const targetInfo = plugin.context.graph?.nodes.get(targetPointer);

process({
meta: {
resource: 'definition',
resourceId: targetPointer,
},
naming: plugin.config.definitions,
path: targetPath,
plugin,
schema: targetSchema,
tags: targetInfo?.tags ? Array.from(targetInfo.tags) : undefined,
});

if (!plugin.isSymbolRegistered(query)) {
throw new Error(`Failed to emit acyclic Valibot schema reference "${$ref}"`);
}
}

const schemaExtractor: SchemaExtractor<ProcessorContext> = (ctx) => {
if (processor.hasEmitted(ctx.path)) {
return ctx.schema;
Expand All @@ -39,7 +93,12 @@ export function createProcessor(plugin: ValibotPlugin['Instance']): ProcessorRes
const shouldExport = ctx.export !== false;

return processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => {
const visitor = createVisitor({ plugin, schemaExtractor });
const currentPointer = pathToJsonPointer(ctx.path);
const visitor = createVisitor({
ensureReferenceRegistered: ($ref) => ensureReferenceRegistered(currentPointer, $ref),
plugin,
schemaExtractor,
});
const walk = createSchemaWalker(visitor);

const result = walk(ctx.schema, {
Expand Down
11 changes: 7 additions & 4 deletions packages/openapi-ts/src/plugins/valibot/v1/visitor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SymbolMeta } from '@hey-api/codegen-core';
import { fromRef } from '@hey-api/codegen-core';
import type { SchemaExtractor, SchemaVisitor } from '@hey-api/shared';
import { pathToJsonPointer } from '@hey-api/shared';
import { normalizeJsonPointer, pathToJsonPointer } from '@hey-api/shared';

import { $ } from '../../../ts-dsl';
import { maybeBigInt } from '../../shared/utils/coerce';
Expand All @@ -27,6 +27,8 @@ import { unknownToPipes } from './toAst/unknown';
import { voidToPipes } from './toAst/void';

export interface VisitorConfig {
/** Ensures an acyclic referenced schema has been emitted before using it directly. */
ensureReferenceRegistered?: ($ref: string) => void;
/** The plugin instance. */
plugin: ValibotPlugin['Instance'];
/** Optional schema extractor function. */
Expand All @@ -40,7 +42,7 @@ function getDefaultValue(meta: ValibotMeta): ReturnType<typeof $.fromValue> {
export function createVisitor(
config: VisitorConfig,
): SchemaVisitor<ValibotResult, ValibotPlugin['Instance']> {
const { plugin, schemaExtractor } = config;
const { ensureReferenceRegistered, plugin, schemaExtractor } = config;

const v = plugin.imports.v;

Expand Down Expand Up @@ -251,15 +253,16 @@ export function createVisitor(
};
},
reference($ref, schema) {
const resourceId = normalizeJsonPointer($ref);
const query: SymbolMeta = {
artifact: 'valibot',
category: 'schema',
resource: 'definition',
resourceId: $ref,
resourceId,
};
ensureReferenceRegistered?.($ref);
// TODO: contract (self)
const refSymbol = plugin.referenceSymbol(query);
// TODO: contract (self)
const isRegistered = plugin.isSymbolRegistered(query);

if (isRegistered) {
Expand Down
23 changes: 23 additions & 0 deletions specs/3.1.x/validators-acyclic-ref-order.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
openapi: 3.1.0
info:
title: OpenAPI 3.1.0 validators acyclic reference order example
version: 1
paths: {}
components:
schemas:
Parent:
type: object
required:
- id
properties:
id:
type: number
children:
type: array
items:
$ref: '#/components/schemas/Child'
Child:
type: object
properties:
id:
type: number
Loading