Merge commit '36c66dd290d3ce6eb1ccd310d0c658d4a32bb8eb' as 'repos/effect'
This commit is contained in:
154
repos/effect/packages/tools/ai-codegen/src/SpecFetcher.ts
Normal file
154
repos/effect/packages/tools/ai-codegen/src/SpecFetcher.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 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<unknown, SpecFetchError>
|
||||
}
|
||||
|
||||
/**
|
||||
* Service tag for fetching OpenAPI specifications from configured sources.
|
||||
*
|
||||
* @category services
|
||||
* @since 4.0.0
|
||||
*/
|
||||
export const SpecFetcher: Context.Service<SpecFetcher, SpecFetcher> = 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))
|
||||
Reference in New Issue
Block a user