From f165ab468d002577e33d242b8d21a72da5068204 Mon Sep 17 00:00:00 2001 From: Tate Barber Date: Thu, 28 Aug 2025 14:28:51 -0500 Subject: [PATCH] fix: Support encoded refs --- src/JsonSchemaGen.ts | 8 ++++---- src/OpenApi.ts | 4 ++-- src/Utils.ts | 24 ++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/JsonSchemaGen.ts b/src/JsonSchemaGen.ts index deb197e..d9d2775 100644 --- a/src/JsonSchemaGen.ts +++ b/src/JsonSchemaGen.ts @@ -5,7 +5,7 @@ import * as Option from "effect/Option" import * as Layer from "effect/Layer" import * as Arr from "effect/Array" import { pipe } from "effect/Function" -import { identifier, nonEmptyString, toComment } from "./Utils" +import { identifier, nonEmptyString, toComment, decodeRefTokens, refLastToken } from "./Utils" import * as Struct from "effect/Struct" const make = Effect.gen(function* () { @@ -135,7 +135,7 @@ const make = Effect.gen(function* () { if ("$ref" in root) { addRefs(root, undefined, false) - return identifier(root.$ref.split("/").pop()!) + return identifier(refLastToken(root.$ref)) } else { addRefs(root, "properties" in root ? name : undefined) store.set(name, root) @@ -278,7 +278,7 @@ const make = Effect.gen(function* () { if (!schema.$ref.startsWith("#")) { return Option.none() } - const name = identifier(schema.$ref.split("/").pop()!) + const name = identifier(refLastToken(schema.$ref)) return Option.some(transformer.onRef({ importName, name })) } else if ("properties" in schema) { return toSource( @@ -789,7 +789,7 @@ function resolveRef( if (!schema.$ref.startsWith("#")) { return } - const path = schema.$ref.slice(2).split("/") + const path = decodeRefTokens(schema.$ref) const name = identifier(path[path.length - 1]) let current = context diff --git a/src/OpenApi.ts b/src/OpenApi.ts index b84f981..7e78a91 100644 --- a/src/OpenApi.ts +++ b/src/OpenApi.ts @@ -8,7 +8,7 @@ import * as Layer from "effect/Layer" import * as JsonSchemaGen from "./JsonSchemaGen.js" import type * as JsonSchema from "@effect/platform/OpenApiJsonSchema" import type { DeepMutable } from "effect/Types" -import { camelize, identifier, nonEmptyString, toComment } from "./Utils.js" +import { camelize, identifier, nonEmptyString, toComment, decodeRefTokens } from "./Utils.js" import { convertObj } from "swagger2openapi" import * as Context from "effect/Context" import * as Option from "effect/Option" @@ -91,7 +91,7 @@ export const make = Effect.gen(function* () { const operations: Array = [] function resolveRef(ref: string) { - const parts = ref.split("/").slice(1) + const parts = decodeRefTokens(ref) let current: any = spec for (const part of parts) { current = current[part] diff --git a/src/Utils.ts b/src/Utils.ts index d3097ad..8c779cf 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -41,3 +41,27 @@ export const toComment = Option.match({ * ${description.replace(/\*\//g, " * /").split("\n").join("\n* ")} */\n`, }) + +// Decode an OpenAPI $ref JSON Pointer fragment into path tokens. +// Handles RFC3986 percent-decoding and RFC6901 JSON Pointer escapes (~0/~1). +export const decodeRefTokens = (ref: string): ReadonlyArray => { + if (!ref) return [] + let fragment = ref.startsWith("#") ? ref.slice(1) : ref + if (fragment.startsWith("/")) fragment = fragment.slice(1) + if (fragment.length === 0) return [] + return fragment.split("/").map((raw) => { + let token = raw + try { + token = decodeURIComponent(raw) + } catch { + // leave as-is if not a valid percent-encoded sequence + } + // Unescape JSON Pointer tokens per RFC6901 + return token.replace(/~1/g, "/").replace(/~0/g, "~") + }) +} + +export const refLastToken = (ref: string): string => { + const tokens = decodeRefTokens(ref) + return tokens.length > 0 ? tokens[tokens.length - 1] : ref +}