/** * Service for fetching OpenAPI specifications from URLs or the filesystem. * * @since 4.0.0 */ import * as Context from "effect/Context" import * as Data from "effect/Data" import * as Effect from "effect/Effect" import * as FileSystem from "effect/FileSystem" import * as Layer from "effect/Layer" import * as Match from "effect/Match" import * as HttpClient from "effect/unstable/http/HttpClient" import * as Yaml from "yaml" import type { SpecSource } from "./Config.ts" /** * Error when fetching a spec fails. * * **Example** (Creating a spec fetch error) * * ```ts * import * as SpecFetcher from "@effect/ai-codegen/SpecFetcher" * * const error = new SpecFetcher.SpecFetchError({ * provider: "openai", * source: "https://example.com/openapi.json", * cause: new Error("Network error") * }) * ``` * * @category errors * @since 4.0.0 */ export class SpecFetchError extends Data.TaggedError("SpecFetchError")<{ readonly provider: string readonly source: string readonly cause: unknown }> {} /** * Service for fetching OpenAPI specifications. * * @category models * @since 4.0.0 */ export interface SpecFetcher { readonly fetch: ( source: SpecSource, provider: string ) => Effect.Effect } /** * Service tag for fetching OpenAPI specifications from configured sources. * * @category services * @since 4.0.0 */ export const SpecFetcher: Context.Service = Context.Service( "@effect/ai-codegen/SpecFetcher" ) /** * Layer providing the SpecFetcher service. * * @category layers * @since 4.0.0 */ export const layer: Layer.Layer< SpecFetcher, never, FileSystem.FileSystem | HttpClient.HttpClient > = Effect.gen(function*() { const fs = yield* FileSystem.FileSystem const httpClient = yield* HttpClient.HttpClient const fetchFromFile = Effect.fn("fetchFromFile")(function*( path: string, provider: string ) { return yield* fs.readFileString(path).pipe( Effect.mapError((cause) => new SpecFetchError({ provider, source: path, cause })) ) }) const fetchFromUrl = Effect.fn("fetchFromUrl")(function*( url: string, provider: string ) { return yield* httpClient.get(url).pipe( Effect.flatMap((response) => response.text), Effect.mapError((cause) => new SpecFetchError({ provider, source: url, cause })) ) }) const parseSpec = (content: string, sourceUrl: string, provider: string) => { const isYaml = sourceUrl.endsWith(".yaml") || sourceUrl.endsWith(".yml") return Effect.try({ try: () => isYaml ? Yaml.parse(content) : JSON.parse(content), catch: (cause) => new SpecFetchError({ provider, source: sourceUrl, cause }) }) } const fetchViaStainlessStats = Effect.fn("fetchViaStainlessStats")(function*( statsUrl: string, provider: string ) { // Fetch stats.yml const statsContent = yield* httpClient.get(statsUrl).pipe( Effect.flatMap((response) => response.text), Effect.mapError((cause) => new SpecFetchError({ provider, source: `stats:${statsUrl}`, cause })) ) // Parse YAML and extract openapi_spec_url const stats = yield* Effect.try({ try: () => Yaml.parse(statsContent), catch: (cause) => new SpecFetchError({ provider, source: `stats:${statsUrl}`, cause }) }) const specUrl = stats?.openapi_spec_url if (typeof specUrl !== "string") { return yield* new SpecFetchError({ provider, source: `stats:${statsUrl}`, cause: new Error("Missing or invalid openapi_spec_url in stats file") }) } // Fetch and parse the actual spec const content = yield* fetchFromUrl(specUrl, provider) return yield* parseSpec(content, specUrl, provider) }) const fetch = Effect.fn("fetch")(function*( source: SpecSource, provider: string ) { // StainlessStats handles its own parsing since the URL is resolved dynamically if (source._tag === "StainlessStats") { return yield* fetchViaStainlessStats(source.statsUrl, provider) } const content = yield* Match.value(source).pipe( Match.tag("File", ({ path }) => fetchFromFile(path, provider)), Match.tag("Url", ({ url }) => fetchFromUrl(url, provider)), Match.exhaustive ) const sourceString = source._tag === "Url" ? source.url : source.path return yield* parseSpec(content, sourceString, provider) }) return { fetch } }).pipe(Layer.effect(SpecFetcher))