Skip to content
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
60 changes: 60 additions & 0 deletions src/components/document/ModelNode.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, it, expect } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import ModelNode from './ModelNode.vue'
import type { SchemaObject } from '@/types'

const data: SchemaObject = {
description: "I'm a model's description.",
type: 'object',
title: 'Todo',
example: {
id: 1,
name: 'Buy milk',
completed: true,
completed_at: '2021-01-01T00:00:00.000Z',
},
properties: {
id: {
type: 'number',
minimum: 0,
maximum: 9999,
description: 'ID of the task',
readOnly: true,
},
name: {
type: 'string',
minLength: 1,
maxLength: 100,
description: 'Name of the task',
},
completed: {
type: 'boolean',
default: false,
description: 'Boolean indicating if the task has been completed or not',
},
completed_at: {
type: 'string',
format: 'date-time',
description: 'Time when the task was completed',
readOnly: true,
},
},
required: ['id', 'name'],
}

const title = 'Todo'

describe('<ModelNode />', () => {
it('renders all properties of a model', () => {
const wrapper = shallowMount(ModelNode, {
props: {
data,
title,
},
})

for (const property in data.properties) {
expect(wrapper.findTestId(`model-property-${property}`).exists()).toBe(true)
}
})
})
41 changes: 14 additions & 27 deletions src/components/document/ModelNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,25 @@
<span>Allowed values: </span> {{ data.enum }}
</p>

<ModelProperties
v-if="modelPropertiesProps"
:properties="modelPropertiesProps.properties"
:required-fields="modelPropertiesProps.required"
/>
<template v-if="modelPropertyProps">
<ModelProperty
v-for="(property, propertyName) in modelPropertyProps.properties"
:key="propertyName"
:data-testid="`model-property-${propertyName}`"
:property="property"
:property-name="propertyName.toString()"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Background:
#33 (comment)

if the type of propertyName is a string, why do we need toString here?

@adamdehaven turns out it's because openapi3-ts has typed properties as a mapped type { [propertyName: string]: SchemaObject | ReferenceObject }, instead of a Record<string, SchemaObject | ReferenceObject>

It's a known TS quirk that keys in a mapped type will always be interpreted as string | number. Ref: microsoft/TypeScript#48269

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have opened a PR in openapi3-ts to fix this

metadevpro/openapi3-ts#135

:required-fields="modelPropertyProps.required"
/>
</template>
</template>

<script setup lang="ts">
import { computed, toRefs } from 'vue'
import ModelProperties from './ModelProperties.vue'

import { isValidSchemaObject } from '@/utils'
import { computed } from 'vue'
import ModelProperty from './ModelProperty.vue'

import type{ PropType } from 'vue'
import type { SchemaObject } from '@/types'
import { schemaObjectProperties } from '@/utils'

const props = defineProps({
data: {
Expand All @@ -39,22 +43,5 @@ const props = defineProps({
},
})

const modelPropertiesProps = computed(() => {
const { data } = toRefs(props)
let computedObj: Partial<SchemaObject> | null = null

/**
* We have to enumerate over the properties of the Schema Model and render them out via `ModelProperties` component.
* For this, we need to compute the properties and required fields of the Schema Model.
* If the top level Schema Model is an object, we can directly use the `properties` field of the object.
* If it's an array, we need to derive the properties from the `items` field of the Schema Model.
*/
if (data.value.type === 'object' && data.value.properties && Reflect.ownKeys(data.value.properties).length) {
computedObj = { properties: data.value.properties, required: data.value.required }
} else if (data.value.type === 'array' && isValidSchemaObject(data.value.items)) {
computedObj = { properties: data.value.items.properties, required: data.value.items.required }
}

return computedObj
})
const modelPropertyProps = computed(() => schemaObjectProperties(props.data))
</script>
108 changes: 0 additions & 108 deletions src/components/document/ModelProperties.vue

This file was deleted.

117 changes: 117 additions & 0 deletions src/components/document/ModelProperty.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import ModelProperty from './ModelProperty.vue'

describe('<ModelProperty />', () => {
it('renders all fields of a property', () => {
const wrapper = mount(ModelProperty, {
props: {
property: {
type: 'integer',
format: 'int32',
description: 'sample description',
example: 'lorem ipsum',
enum: [100, 200, 300],
pattern: '^[0-9]{3}$',
maximum: 999,
minimum: 100,
items: {
type: 'string',
},
},
propertyName: 'sample-property',
requiredFields: ['sample-property', 'property-a', 'property-b'],
},
})

const componentList = [
'property-field-description',
'property-field-example',
'property-field-enum',
'property-field-info',
'property-field-pattern',
'property-field-range',
]

for (const component of componentList) {
expect(wrapper.findTestId(component).exists()).toBe(true)
}
})

it('renders all fields of a nested property', () => {
const wrapper = mount(ModelProperty, {
props: {
property: {
type: 'object',
description: 'Period of time data is returned for.',
properties: {
start: {
type: 'string',
format: 'date-time',
description:
"Timestamp specifying the lower bound of the query's time range.",
},
end: {
type: 'string',
format: 'date-time',
description:
"Timestamp specifying the upper bound of the query's time range.",
},
},
},
propertyName: 'time-range',
requiredFields: ['time_range', 'query_id', 'size', 'offset'],
},
})

const componentList = [
// top level property
'model-property-time-range',
// nested properties
'model-property-start',
'model-property-end',
]

for (const component of componentList) {
expect(wrapper.findTestId(component).exists()).toBe(true)
}
})

it('renders all fields of items of an array property', () => {
const wrapper = mount(ModelProperty, {
props: {
property: {
type: 'array',
description: 'A sample array description',
items: {
properties: {
'sample-item-1': {
type: 'integer',
format: 'int32',
example: '34',
},
'sample-item-2': {
type: 'string',
example: 'abc',
},
},
required: ['sample-item-1'],
},
},
propertyName: 'sample-property',
},
})

const componentList = [
// top level property
'model-property-sample-property',
// nested item properties
'model-property-sample-item-1',
'model-property-sample-item-2',
]

for (const component of componentList) {
expect(wrapper.findTestId(component).exists()).toBe(true)
}
})
})
Loading