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: improves indirect recursion detection #46

Merged
merged 2 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 additions & 0 deletions docs/astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ export default defineConfig({
label: 'Animals v2.0',
schema: '../schemas/v2.0/animals.yaml',
},
{
base: 'api/v3/recursive',
label: 'Recursion v3.0',
schema: '../schemas/v3.0/recursive.yaml',
},
{
base: 'api/v3/recursive-simple',
label: 'Simple Recursion v3.0',
schema: '../schemas/v3.0/recursive-simple.yaml',
},
]),
starlightOpenAPIDocsDemoPlugin(),
],
Expand Down
5 changes: 3 additions & 2 deletions packages/starlight-openapi/components/Items.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ interface Props {
items: Items
negated?: boolean | undefined
nullable?: boolean | undefined
parents?: SchemaObject[]
}

const { hideExample, items, negated, nullable } = Astro.props
const { hideExample, items, negated, nullable, parents = [] } = Astro.props

const enumItems = items.enum ?? items.items?.enum
---
Expand Down Expand Up @@ -46,7 +47,7 @@ const enumItems = items.enum ?? items.items?.enum
/>

{enumItems && <Tags label="Allowed values:" tags={enumItems} />}
{items.items && isSchemaObjectObject(items.items) && <SchemaObject nested schemaObject={items.items} {hideExample} />}
{items.items && isSchemaObjectObject(items.items) && <SchemaObject {parents} nested schemaObject={items.items} {hideExample} />}

<style>
.type {
Expand Down
11 changes: 6 additions & 5 deletions packages/starlight-openapi/components/schema/SchemaObject.astro
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ interface Props {
hideExample?: boolean | undefined
negated?: boolean
nested?: boolean
parents?: SchemaObject[]
schemaObject: SchemaObject
}

const { hideExample = false, negated, nested = false, schemaObject } = Astro.props
const { hideExample = false, negated, nested = false, parents = [], schemaObject } = Astro.props

const schemaObjects = getSchemaObjects(schemaObject)

Expand All @@ -32,19 +33,19 @@ const isNegated = schemaObject.not !== undefined

{
hasMany ? (
<SchemaObjects discriminator={schemaObject.discriminator} {nested} {schemaObjects} />
<SchemaObjects {parents} discriminator={schemaObject.discriminator} {nested} {schemaObjects} />
) : isNegated ? (
<Astro.self negated schemaObject={schemaObject.not} />
) : (
<>
<Md text={schemaObject.description} />
<ExternalDocs docs={schemaObject.externalDocs} />
{isSchemaObjectObject(schemaObject) ? (
<SchemaObjectObject {nested} {schemaObject} />
<SchemaObjectObject {parents} {nested} {schemaObject} />
) : isSchemaObjectAllOf(schemaObject) ? (
<SchemaObjectAllOf {schemaObject} />
<SchemaObjectAllOf {parents} {schemaObject} />
) : (
<Items items={schemaObject} {negated} nullable={getNullable(schemaObject)} />
<Items {parents} items={schemaObject} {negated} nullable={getNullable(schemaObject)} />
)}
{!hideExample && schemaObject.example && <Example raw={schemaObject.example} />}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,19 @@ import Items from '../Items.astro'
import SchemaObjectObjectProperties from './SchemaObjectObjectProperties.astro'

interface Props {
parents?: SchemaObject[]
schemaObject: SchemaObject
}

const { schemaObject } = Astro.props
const { schemaObject, parents = [] } = Astro.props
---

{
schemaObject.allOf &&
schemaObject.allOf.map((allOfSchemaObject) =>
isSchemaObjectObject(schemaObject) && isSchemaObject(allOfSchemaObject) ? (
<SchemaObjectObjectProperties
parent={schemaObject}
parents={[...parents, schemaObject]}
properties={getProperties(allOfSchemaObject)}
required={allOfSchemaObject.required}
/>
Expand All @@ -31,6 +32,7 @@ const { schemaObject } = Astro.props
items={allOfSchemaObject}
negated={allOfSchemaObject.not !== undefined}
nullable={getNullable(allOfSchemaObject)}
parents={[...parents, schemaObject]}
/>
) : null,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import SchemaObjectObjectProperties from './SchemaObjectObjectProperties.astro'

interface Props {
nested: boolean
parents?: SchemaObject[]
schemaObject: SchemaObject
}

const { nested, schemaObject } = Astro.props
const { nested, parents = [], schemaObject } = Astro.props

const properties = getProperties(schemaObject)
---
Expand All @@ -30,15 +31,15 @@ const properties = getProperties(schemaObject)
schemaObject.maxProperties && `<= ${schemaObject.maxProperties} properties`,
]}
/>
<SchemaObjectObjectProperties parent={schemaObject} {properties} required={schemaObject.required} />
<SchemaObjectAllOf {schemaObject} />
<SchemaObjectObjectProperties parents={[...parents, schemaObject]} {properties} required={schemaObject.required} />
<SchemaObjectAllOf {parents} {schemaObject} />
{
schemaObject.additionalProperties && (
<Key additional name="key">
{schemaObject.additionalProperties === true ? (
<div class="any">any</div>
) : isAdditionalPropertiesWithSchemaObject(schemaObject.additionalProperties) ? (
<Schema schemaObject={schemaObject.additionalProperties} />
<Schema parents={[...parents, schemaObject]} schemaObject={schemaObject.additionalProperties} />
) : null}
</Key>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@ import Tag from '../Tag.astro'
import Schema from './SchemaObject.astro'

interface Props {
parent: SchemaObject
parents: SchemaObject[]
properties: Properties
required: string[] | undefined
}

const { parent, properties, required } = Astro.props
const { parents, properties, required } = Astro.props
---

{
Object.entries(properties).map(([name, schema]) => (
<Key name={name} required={required?.includes(name)}>
{schema === parent ? (
{parents?.indexOf(schema) >= 0 ? (
<div>
<span class="type">object</span>
<Tag>recursive</Tag>
</div>
) : (
<Schema nested schemaObject={schema} />
<Schema {parents} nested schemaObject={schema} />
)}
</Key>
))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import SchemaObject from './SchemaObject.astro'
interface Props {
discriminator: Discriminator
nested: boolean
parents?: SchemaObject[]
schemaObjects: SchemaObjects
}

const {
discriminator,
nested,
parents = [],
schemaObjects: { schemaObjects, type },
} = Astro.props

Expand All @@ -40,7 +42,7 @@ const humanReadableType: Record<SchemaObjects['type'], string> = {
{
schemaObjects.map((schemaObject) => (
<TabItem label={schemaObject.title ?? getType(schemaObject) ?? 'unknown'}>
<SchemaObject {nested} schemaObject={schemaObject} />
<SchemaObject {parents} {nested} schemaObject={schemaObject} />
</TabItem>
))
}
Expand Down
19 changes: 19 additions & 0 deletions packages/starlight-openapi/tests/recursion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { expect, test } from './test'

test('displays the recursive tag for a recursive category schema', async ({ docPage }) => {
await docPage.goto('/v3/recursive/operations/listcategories')
const okResponse = docPage.getResponse('200')
await expect(okResponse.getByText('recursive')).toHaveCount(1)
})

test('displays the recursive tag for a recursive post schema', async ({ docPage }) => {
await docPage.goto('/v3/recursive/operations/listposts')
const okResponse = docPage.getResponse('200')
await expect(okResponse.getByText('recursive')).toHaveCount(1)
})

test('displays the recursive tag for a simpler recursive category schema', async ({ docPage }) => {
await docPage.goto('/v3/recursive-simple/operations/listcategories')
const okResponse = docPage.getResponse('200')
await expect(okResponse.getByText('recursive')).toHaveCount(1)
})
72 changes: 72 additions & 0 deletions schemas/v3.0/recursive-simple.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
openapi: 3.1.0
info:
title: Test Simple Recursion
description: Example of the simple recursion issue.
version: 1.0.0
servers:
- url: 'http://localhost'
paths:
/categories:
get:
summary: List all categories
operationId: listCategories
parameters:
- name: limit
in: query
description: How many categories to return at one time (max 100)
schema:
type: integer
maximum: 50
format: int32
nullable: true
- name: offset
in: query
description: The number of categories to skip before starting to collect the result set
required: false
schema:
type: integer
format: int32
responses:
'200':
description: A paged array of categories
content:
application/json:
schema:
$ref: '#/components/schemas/Categories'
default:
description: unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
Category:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int64
name:
type: string
parent:
$ref: '#/components/schemas/Category'
Categories:
type: array
maxItems: 100
items:
$ref: '#/components/schemas/Category'
Error:
type: object
required:
- code
- message
properties:
code:
type: integer
format: int32
message:
type: string
Loading