Skip to content

Commit

Permalink
Integration with Scalar (#3856)
Browse files Browse the repository at this point in the history
Co-authored-by: maksim.khramtsov <[email protected]>
Co-authored-by: Tim <[email protected]>
  • Loading branch information
3 people authored Oct 29, 2024
1 parent 3db5a97 commit 81ddd45
Show file tree
Hide file tree
Showing 10 changed files with 224 additions and 9 deletions.
5 changes: 5 additions & 0 deletions .changeset/small-lobsters-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@effect/platform": patch
---

Integration with [Scalar](https://scalar.com/) has been implemented
12 changes: 6 additions & 6 deletions packages/platform-node/test/fixtures/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
}
},
"400": {
"description": "HttpApiDecodeError: The request did not match the expected schema",
"description": "The request did not match the expected schema",
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -180,7 +180,7 @@
}
},
"400": {
"description": "HttpApiDecodeError: The request did not match the expected schema",
"description": "The request did not match the expected schema",
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -257,7 +257,7 @@
}
},
"400": {
"description": "HttpApiDecodeError: The request did not match the expected schema",
"description": "The request did not match the expected schema",
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -297,7 +297,7 @@
}
},
"400": {
"description": "HttpApiDecodeError: The request did not match the expected schema",
"description": "The request did not match the expected schema",
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -366,7 +366,7 @@
}
},
"400": {
"description": "HttpApiDecodeError: The request did not match the expected schema",
"description": "The request did not match the expected schema",
"content": {
"application/json": {
"schema": {
Expand Down Expand Up @@ -431,7 +431,7 @@
"description": "a string that will be parsed into a number"
},
"HttpApiDecodeError": {
"description": "HttpApiDecodeError: The request did not match the expected schema",
"description": "The request did not match the expected schema",
"title": "HttpApiDecodeError",
"type": "object",
"required": ["issues", "message", "_tag"],
Expand Down
2 changes: 1 addition & 1 deletion packages/platform/src/HttpApiError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class HttpApiDecodeError extends Schema.TaggedError<HttpApiDecodeError>()
},
HttpApiSchema.annotations({
status: 400,
description: "HttpApiDecodeError: The request did not match the expected schema"
description: "The request did not match the expected schema"
})
) {
/**
Expand Down
187 changes: 187 additions & 0 deletions packages/platform/src/HttpApiScalar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/**
* @since 1.0.0
*/
import * as Effect from "effect/Effect"
import type { Layer } from "effect/Layer"
import { Api } from "./HttpApi.js"
import { Router } from "./HttpApiBuilder.js"
import * as HttpServerResponse from "./HttpServerResponse.js"
import * as internal from "./internal/httpApiScalar.js"
import * as OpenApi from "./OpenApi.js"

/**
* @since 1.0.0
* @category model
*/
export type ScalarThemeId =
| "alternate"
| "default"
| "moon"
| "purple"
| "solarized"
| "bluePlanet"
| "deepSpace"
| "saturn"
| "kepler"
| "mars"
| "none"

/**
* @since 1.0.0
* @category model
*
* cdn: `https://cdn.jsdelivr.net/npm/@scalar/api-reference@${source.version}/dist/browser/standalone.min.js`
*/
export type ScalarScriptSource =
| string
| { type: "default" }
| {
type: "cdn"
version?: "latest" | (string & {})
}

/**
* @since 1.0.0
* @category model
* @see https://github.com/scalar/scalar/blob/main/documentation/configuration.md
*/
export type ScalarConfig = {
/** A string to use one of the color presets */
theme?: ScalarThemeId
/** The layout to use for the references */
layout?: "modern" | "classic"
/** URL to a request proxy for the API client */
proxy?: string
/** Whether the spec input should show */
isEditable?: boolean
/** Whether to show the sidebar */
showSidebar?: boolean
/**
* Whether to show models in the sidebar, search, and content.
*
* @default false
*/
hideModels?: boolean
/**
* Whether to show the “Download OpenAPI Document” button
*
* @default false
*/
hideDownloadButton?: boolean
/**
* Whether to show the “Test Request” button
*
* @default: false
*/
hideTestRequestButton?: boolean
/**
* Whether to show the sidebar search bar
*
* @default: false
*/
hideSearch?: boolean
/** Whether dark mode is on or off initially (light mode) */
darkMode?: boolean
/** forceDarkModeState makes it always this state no matter what*/
forceDarkModeState?: "dark" | "light"
/** Whether to show the dark mode toggle */
hideDarkModeToggle?: boolean
/**
* Path to a favicon image
*
* @default undefined
* @example '/favicon.svg'
*/
favicon?: string
/** Custom CSS to be added to the page */
customCss?: string
/**
* The baseServerURL is used when the spec servers are relative paths and we are using SSR.
* On the client we can grab the window.location.origin but on the server we need
* to use this prop.
*
* @default undefined
* @example 'http://localhost:3000'
*/
baseServerURL?: string
/**
* We’re using Inter and JetBrains Mono as the default fonts. If you want to use your own fonts, set this to false.
*
* @default true
*/
withDefaultFonts?: boolean
/**
* By default we only open the relevant tag based on the url, however if you want all the tags open by default then set this configuration option :)
*
* @default false
*/
defaultOpenAllTags?: boolean
}

/**
* @since 1.0.0
* @category layers
*/
export const layer = (options?: {
readonly path?: `/${string}` | undefined
readonly source?: ScalarScriptSource
readonly scalar?: ScalarConfig
}): Layer<never, never, Api> =>
Router.use((router) =>
Effect.gen(function*() {
const { api } = yield* Api
const spec = OpenApi.fromApi(api)

const source = options?.source
const defaultScript = internal.javascript
const src: string | null = source
? typeof source === "string"
? source
: source.type === "cdn"
? `https://cdn.jsdelivr.net/npm/@scalar/api-reference@${
source.version ?? "latest"
}/dist/browser/standalone.min.js`
: null
: null

const scalarConfig = {
_integration: "http",
...options?.scalar
}

const response = HttpServerResponse.html(`<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>${spec.info.title}</title>
${
!spec.info.description
? ""
: `<meta name="description" content="${spec.info.description}"/>`
}
${
!spec.info.description
? ""
: `<meta name="og:description" content="${spec.info.description}"/>`
}
<meta
name="viewport"
content="width=device-width, initial-scale=1" />
</head>
<body>
<script id="api-reference" type="application/json">
${JSON.stringify(spec)}
</script>
<script>
document.getElementById('api-reference').dataset.configuration = JSON.stringify(${JSON.stringify(scalarConfig)})
</script>
${
src
? `<script src="${src}" crossorigin></script>`
: `<script>${defaultScript}</script>`
}
</body>
</html>`)
yield* router.get(options?.path ?? "/docs", Effect.succeed(response))
})
)
2 changes: 1 addition & 1 deletion packages/platform/src/HttpApiSwagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { Layer } from "effect/Layer"
import { Api } from "./HttpApi.js"
import { Router } from "./HttpApiBuilder.js"
import * as HttpServerResponse from "./HttpServerResponse.js"
import * as internal from "./internal/apiSwagger.js"
import * as internal from "./internal/httpApiSwagger.js"
import * as OpenApi from "./OpenApi.js"

/**
Expand Down
5 changes: 5 additions & 0 deletions packages/platform/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export * as HttpApiGroup from "./HttpApiGroup.js"
*/
export * as HttpApiMiddleware from "./HttpApiMiddleware.js"

/**
* @since 1.0.0
*/
export * as HttpApiScalar from "./HttpApiScalar.js"

/**
* @since 1.0.0
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/platform/src/internal/httpApiScalar.ts

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions scripts/package-scalar.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* eslint-disable no-undef */
import * as Fs from "fs/promises"

const jsBundle = await fetch(
"https://cdn.jsdelivr.net/npm/@scalar/api-reference@latest/dist/browser/standalone.min.js"
).then((res) => res.text())

const source = `/* eslint-disable */
/** @internal */
export const javascript = ${JSON.stringify(`${jsBundle}`)}
`

await Fs.writeFile("packages/platform/src/internal/httpApiScalar.ts", source)
2 changes: 1 addition & 1 deletion scripts/package-swagger.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ export const javascript = ${JSON.stringify(`${jsBundle}\n${jsPreset}`)}
export const css = ${JSON.stringify(css)}
`

await Fs.writeFile("packages/platform/src/internal/apiSwagger.ts", source)
await Fs.writeFile("packages/platform/src/internal/httpApiSwagger.ts", source)

0 comments on commit 81ddd45

Please sign in to comment.