Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: schema examples #149

Merged
merged 5 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from 4 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
38 changes: 38 additions & 0 deletions docs/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,24 @@
"application/json": {
"schema": {
"$ref": "#/components/schemas/Artist"
},
"examples": {
"Charly": {
"value": {
"name": "Charly García",
"description": "One of the most influential rock musicians in Argentine history.",
"image": "https://cdn.rock-legends.com/photos/charly.jpg",
"band": "Sui Generis"
}
},
"Maria": {
"value": {
"name": "Maria Gabriela Epumer",
"description": "A very important rock musician in Argentine history.",
"image": "https://cdn.rock-legends.com/photos/maria.jpg",
"band": "Viudas e Hijas de Roque Enroll"
}
}
}
}
}
Expand All @@ -114,6 +132,26 @@
"application/json": {
"schema": {
"$ref": "#/components/schemas/Artist"
},
"examples": {
"Charly": {
"value": {
"id": 1,
"name": "Charly García",
"description": "One of the most influential rock musicians in Argentine history.",
"image": "https://cdn.rock-legends.com/photos/charly.jpg",
"band": "Sui Generis"
}
},
"Maria": {
"value": {
"id": 2,
"name": "Maria Gabriela Epumer",
"description": "A very important rock musician in Argentine history.",
"image": "https://cdn.rock-legends.com/photos/maria.jpg",
"band": "Viudas e Hijas de Roque Enroll"
}
}
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions src/components/Path/OAPath.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ const openapi = props.openapi ?? getOpenApiInstance()
const operation = openapi.getOperation(props.id)
const operationData = initOperationData(operation)
provide('operationData', operationData)
const operationPath = openapi.getOperationPath(props.id)
const operationMethod = openapi.getOperationMethod(props.id)?.toUpperCase()
Expand Down Expand Up @@ -85,7 +89,7 @@ const request = ref(
baseUrl: baseUrl.value,
parameters: operationParameters ?? [],
authorizations: securityUi.length ? Object.entries(securityUi)[0]?.schemes : {},
body: operationRequestBody?.content?.[bodyRequestContentType]?.uiContentType,
body: (operationRequestBody?.content?.[bodyRequestContentType]?.examples ?? { value: '' })[0]?.value,
variables: {},
})
: {},
Expand All @@ -98,8 +102,6 @@ function updateRequest(newRequest) {
function updateSelectedServer(server) {
selectedServer.value = server
}
provide('operationData', initOperationData(operation))
</script>
<template>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Playground/OAPlaygroundParameters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const props = defineProps({
type: Object,
required: true,
},
schemaUiContentType: {
examples: {
type: Object,
required: false,
},
Expand Down Expand Up @@ -84,7 +84,7 @@ const operationData = inject('operationData') as OperationData
const authorizations = ref<PlaygroundSecurityScheme[] | null>(null)
const body = ref(props.schemaUiContentType)
const body = ref(Object.keys(props.examples ?? {}).length ? Object.values(props.examples ?? {})[0].value : null)
function setAuthorizations(schemes: Record<string, PlaygroundSecurityScheme>) {
if (!schemes || !Object.keys(schemes).length) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Request/OARequestBody.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const props = defineProps({
<template>
<OASchemaTabs
:schema="props.requestBody?.content?.[props.contentType]?.ui"
:schema-ui-content-type="props.requestBody?.content?.[props.contentType]?.uiContentType"
:examples="props.requestBody?.content?.[props.contentType]?.examples"
:content-type="props.contentType"
:is-dark="props.isDark"
/>
Expand Down
4 changes: 2 additions & 2 deletions src/components/Response/OAResponse.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const contentType = ref(contentTypes[0] ?? undefined)

const schema = computed(() => props.response.content?.[contentType.value]?.ui)

const schemaJson = computed(() => props.response.content?.[contentType.value]?.uiContentType)
const examples = computed(() => props.response.content?.[contentType.value]?.examples)

const contentTypeId = `content-type-${Math.random().toString(36).substring(7)}`
</script>
Expand Down Expand Up @@ -85,7 +85,7 @@ const contentTypeId = `content-type-${Math.random().toString(36).substring(7)}`
<OASchemaTabs
v-if="schema"
:schema="schema"
:schema-ui-content-type="schemaJson"
:examples="examples"
:content-type="contentType"
:is-dark="props.isDark"
/>
Expand Down
106 changes: 35 additions & 71 deletions src/components/Schema/OASchemaTabs.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
<script setup>
import { computed, ref } from 'vue'
import { computed } from 'vue'
import { useTheme } from '../../composables/useTheme'
import { hasExample } from '../../lib/examples/hasExample'
import { getSchemaUiContentType } from '../../lib/getSchemaUiContentType'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'
import { Checkbox } from '../ui/checkbox'
import { Label } from '../ui/label'
import OASchemaUI from '../Schema/OASchemaUI.vue'
import OAMarkdown from '../Common/OAMarkdown.vue'

const props = defineProps({
schema: {
type: Object,
required: true,
},
schemaUiContentType: {
type: [Object, String, Number, Boolean, Array],
required: true,
},
contentType: {
type: String,
required: true,
examples: {
type: null,
required: false,
},
isDark: {
type: Boolean,
Expand All @@ -29,46 +22,26 @@ const props = defineProps({

const themeConfig = useTheme()

const useExample = ref(true)

const schemaHasExample = hasExample(props.schema)

const checkboxId = `useExample-${Math.random().toString(36).substring(7)}`

const contentTypeLabel = computed(() => {
if (props.contentType.includes('json')) {
return 'JSON'
}
if (props.contentType === 'application/xml') {
return 'XML'
}
return 'Schema'
})

const lang = computed(() => {
if (props.contentType.includes('json')) {
return 'json'
}
if (props.contentType === 'application/xml') {
return 'xml'
}
return props.contentType
})

const hasSchemaContentType = computed(() => props.schemaUiContentType !== null)
const defaultValue = computed(() => {
const defaultView = themeConfig.schemaConfig.defaultView.value
const examplesKeys = Object.keys(props.examples ?? {})

const schemaUiContentType = computed(() => {
if (useExample.value) {
return props.schemaUiContentType
const viewMap = {
schema: 'schema',
contentType: examplesKeys?.length ? `examples.${examplesKeys[0]}` : 'schema',
autogenerated: (() => {
const autogeneratedExample = examplesKeys.find(key => props.examples[key]?.isAutogenerated)
return autogeneratedExample ? `examples.${autogeneratedExample}` : 'schema'
})(),
}

return getSchemaUiContentType(props.contentType, props.schema, useExample.value)
return viewMap[defaultView] ?? defaultView
})
</script>

<template>
<Tabs
:default-value="hasSchemaContentType ? themeConfig.schemaConfig.defaultView : 'schema'"
:default-value="defaultValue"
>
<TabsList class="relative flex flex-row justify-start">
<TabsTrigger
Expand All @@ -79,12 +52,13 @@ const schemaUiContentType = computed(() => {
{{ $t('Schema') }}
</TabsTrigger>
<TabsTrigger
v-if="hasSchemaContentType"
value="contentType"
v-for="example in Object.keys(props.examples ?? {})"
:key="example"
:value="`examples.${example}`"
variant="schemaTabs"
class="h-full"
>
{{ contentTypeLabel }}
{{ example }}
</TabsTrigger>
</TabsList>
<TabsContent value="schema">
Expand All @@ -96,32 +70,22 @@ const schemaUiContentType = computed(() => {
/>
</div>
</TabsContent>
<TabsContent value="contentType">
<div class="relative flex flex-col">
<div
v-if="schemaHasExample"
class="absolute right-14 top-1 z-10 flex flex-row items-center gap-1"
>
<Checkbox
:id="checkboxId"
:checked="useExample"
name="useExample"
aria-label="Use example"
@update:checked="useExample = $event"
/>
<Label
:for="checkboxId"
class="text-gray-800 dark:text-gray-200 text-sm"
>
{{ $t('Use example') }}
</Label>
</div>
<TabsContent
v-for="example in Object.keys(props.examples ?? {})"
:key="example"
:value="`examples.${example}`"
>
<div class="flex flex-col gap-2">
<OAMarkdown
v-if="props.examples[example]?.description"
:content="props.examples[example]?.description"
/>

<OACodeBlock
v-if="hasSchemaContentType"
:code="schemaUiContentType"
:lang="lang"
:label="contentTypeLabel"
v-if="props.examples[example]?.value"
:code="props.examples[example]?.value"
:lang="props.examples[example]?.lang ?? 'json'"
:label="(props.examples[example]?.lang ?? 'json').toUpperCase()"
:is-dark="props.isDark"
class="!my-0"
/>
Expand Down
12 changes: 6 additions & 6 deletions src/components/Try/OATryWithVariables.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,20 @@ const emits = defineEmits([

const loading = ref(false)

const schemaUiContentType = computed(() => {
return props.requestBody?.content?.[props.contentType]?.uiContentType
const schemaExamples = computed(() => {
return props.requestBody?.content?.[props.contentType]?.examples
})

const hasSchemaVariables = computed(() =>
Object.keys(schemaUiContentType.value?.variables ?? {}).length > 0,
const hasBody = computed(() =>
Boolean(props.requestBody),
)

const hasSecuritySchemes = computed(() =>
Object.keys(props.securityUi ?? {}).length > 0,
)

const hasParameters = computed(() =>
Boolean(props.parameters?.length || hasSchemaVariables.value || hasSecuritySchemes.value),
Boolean(props.parameters?.length || hasBody.value || hasSecuritySchemes.value),
)
</script>

Expand All @@ -87,7 +87,7 @@ const hasParameters = computed(() =>
:base-url="props.baseUrl"
:parameters="props.parameters ?? []"
:security-ui="props.securityUi ?? {}"
:schema-ui-content-type="schemaUiContentType"
:examples="schemaExamples"
:is-dark="props.isDark"
@update:request="($event: any) => emits('update:request', $event)"
@update:selected-server="($event: any) => emits('update:selectedServer', $event)"
Expand Down
10 changes: 5 additions & 5 deletions src/composables/useTheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface PathConfig {
}

export interface RequestConfig {
defaultView: Ref<'schema' | 'contentType'>
defaultView: Ref<'schema' | 'contentType' | 'autogenerated'>
}

export interface JsonViewerConfig {
Expand Down Expand Up @@ -120,7 +120,7 @@ export interface UseThemeConfigUnref {
showBaseURL: boolean
}>
requestBody?: Partial<{
defaultView: 'schema' | 'contentType'
defaultView: 'schema' | 'contentType' | 'autogenerated'
}>
jsonViewer?: Partial<{
deep: number
Expand Down Expand Up @@ -230,7 +230,7 @@ const themeConfig: UseThemeConfig = {
showBaseURL: ref<boolean>(false),
},
requestBody: {
defaultView: ref<'schema' | 'contentType'>('contentType'),
defaultView: ref<'schema' | 'contentType' | 'autogenerated'>('contentType'),
},
jsonViewer: {
deep: ref<number>(Number.POSITIVE_INFINITY),
Expand Down Expand Up @@ -446,11 +446,11 @@ export function useTheme(initialConfig: Partial<UseThemeConfigUnref> = {}) {
return themeConfig?.theme?.highlighterTheme
}

function getSchemaDefaultView(): 'schema' | 'contentType' | undefined {
function getSchemaDefaultView(): 'schema' | 'contentType' | 'autogenerated' | undefined {
return themeConfig?.requestBody?.defaultView?.value
}

function setSchemaDefaultView(value: 'schema' | 'contentType') {
function setSchemaDefaultView(value: 'schema' | 'contentType' | 'autogenerated') {
// @ts-expect-error: This is a valid expression.
themeConfig.requestBody.defaultView.value = value
}
Expand Down
38 changes: 38 additions & 0 deletions src/lib/examples/getSchemaExample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { OAProperty } from '../getSchemaUi'
import type { OAExampleObject } from '../../types'
import { getSchemaUiJson } from './getSchemaUiJson'
import { getSchemaUiXml } from './getSchemaUiXml'

export function getSchemaExample(contentType: string, uiProperties: OAProperty[] | OAProperty, useExample = false): Record<string, OAExampleObject> {
if (isXml(contentType)) {
return {
XML: getSchemaExampleValue(contentType, uiProperties, useExample),
}
}

return {
JSON: getSchemaExampleValue(contentType, uiProperties, useExample),
}
}

function getSchemaExampleValue(contentType: string, uiProperties: OAProperty[] | OAProperty, useExample = false): OAExampleObject {
if (isXml(contentType)) {
return {
summary: 'XML',
description: '',
value: getSchemaUiXml(uiProperties, useExample),
isAutogenerated: true,
} as OAExampleObject
}

return {
summary: 'JSON',
description: '',
value: getSchemaUiJson(uiProperties, useExample),
isAutogenerated: true,
} as OAExampleObject
}

function isXml(contentType: string): boolean {
return contentType.toLowerCase().match(/^(text|application)\/.*xml($|;|\\+)/) !== null
}
Loading
Loading