Repository: enzonotario/vitepress-openapi (ts)

Issue:
Show default values for query parameters
### What would you like?

Query parameters can be assigned default values but these do not seem to appear in the documentation.

### Why is this needed?

It gives the reader information about how the API works.

### How could it be implemented?

_No response_

### Other information

```yaml
Types.PaginationParameters.limit:
      name: limit
      in: query
      required: false
      description: Maximum number of items to return
      schema:
        type: integer
        format: int32
        default: 25
      explode: false
    Types.PaginationParameters.offset:
      name: offset
      in: query
      required: false
      description: The number of items to skip before starting to collect the result set
      schema:
        type: integer
        format: int32
        default: 0
      explode: false
```
Allow multiple query param examples
### What would you like?

Provide and display multiple query param examples for endpoints

### Why is this needed?

OAPIspec allows it, but only the first is displayed by this component

### How could it be implemented?

Rather than display one line with one value
![image](https://github.com/user-attachments/assets/6cffc123-a57c-47d9-b591-fe0033100dc8)
Multiple lines could be displayed.

### Other information

_No response_

Pull request description:
feat(parameters): constraints and examples
<!-- Provide a general summary of your changes in the title above. -->

# Description

Shows parameters constraints as key-value.

## Related issues/external references

- Closes #123
- Closes #130 

## Types of changes

- New feature


Patch:
diff --git a/docs/composables/useTheme.md b/docs/composables/useTheme.md
index 6d7a1bd..f749454 100644
--- a/docs/composables/useTheme.md
+++ b/docs/composables/useTheme.md
@@ -91,6 +91,7 @@ export default {
                 avoidCirculars: false, // Avoid circular references when parsing schemas.
                 lazyRendering: false, // Lazy render Paths and Tags components.
                 defaultTag: 'Default', // Default tag to use when a path has no tags.
+                wrapExamples: true, // Wrap examples in a row or show them in a column.
             },
         })
     }
@@ -153,4 +154,4 @@ export default {
 
 | Function        | Description                  | Default Value                                                                                                                             | Allowed Values                                                                                                                                     |
 |-----------------|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
-| `setSpecConfig` | Sets the spec configuration. | `{ groupByTags: true, collapsePaths: false, showPathsSummary: true, avoidCirculars: false, lazyRendering: false, defaultTag: 'Default' }` | `{ groupByTags: boolean, collapsePaths: boolean, showPathsSummary: boolean, avoidCirculars: boolean, lazyRendering: boolean, defaultTag: string }` |
+| `setSpecConfig` | Sets the spec configuration. | `{ groupByTags: true, collapsePaths: false, showPathsSummary: true, avoidCirculars: false, lazyRendering: false, defaultTag: 'Default', wrapExamples: true }` | `{ groupByTags: boolean, collapsePaths: boolean, showPathsSummary: boolean, avoidCirculars: boolean, lazyRendering: boolean, defaultTag: string, wrapExamples: boolean }` |
diff --git a/docs/public/openapi.json b/docs/public/openapi.json
index 2cb8cd4..221c629 100644
--- a/docs/public/openapi.json
+++ b/docs/public/openapi.json
@@ -464,7 +464,12 @@
         "schema": {
           "type": "integer",
           "format": "int64",
-          "default": 10
+          "default": 10,
+          "examples": [
+            10,
+            20,
+            50
+          ]
         }
       },
       "offset": {
@@ -476,7 +481,12 @@
           "type": "integer",
           "format": "int64",
           "default": 0
-        }
+        },
+        "examples": [
+          1,
+          23,
+          456
+        ]
       }
     },
     "responses": {
diff --git a/package.json b/package.json
index 5e3548f..beae8be 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "vitepress-openapi",
   "type": "module",
-  "version": "0.0.3-alpha.50",
+  "version": "0.0.3-alpha.51",
   "packageManager": "pnpm@9.1.1",
   "homepage": "https://vitepress-openapi.vercel.app/",
   "repository": {
diff --git a/src/components/Parameter/OAParameter.vue b/src/components/Parameter/OAParameter.vue
index 9414329..a638f3d 100644
--- a/src/components/Parameter/OAParameter.vue
+++ b/src/components/Parameter/OAParameter.vue
@@ -1,6 +1,8 @@
 <script setup>
-import { getExample } from '../../lib/getExample'
 import OACodeValue from '../Common/OACodeValue.vue'
+import { getConstraints } from '../../lib/constraintsParser'
+import { getExamples } from '../../lib/getExamples'
+import { useTheme } from '../../composables/useTheme'
 
 const props = defineProps({
   parameter: {
@@ -9,7 +11,11 @@ const props = defineProps({
   },
 })
 
-const example = getExample(props.parameter)
+const examples = getExamples(props.parameter)
+
+const constraints = getConstraints(props.parameter.schema)
+
+const wrapExamples = useTheme().getWrapExamples()
 </script>
 
 <template>
@@ -63,12 +69,49 @@ const example = getExample(props.parameter)
       </div>
 
       <div
-        v-if="example"
+        v-if="examples?.length === 1"
         class="flex flex-row space-x-2"
       >
-        <span class="text-sm font-bold">{{ $t('Example') }}</span>
-        <OACodeValue :value="example" />
+        <span class="text-sm">{{ $t('Example') }}</span>
+        <OACodeValue :value="examples[0]" />
       </div>
+      <div
+        v-if="examples?.length > 1"
+        class="flex flex-row flex-wrap gap-2"
+        :class="{
+          'items-center': wrapExamples,
+        }"
+      >
+        <span class="text-sm">{{ $t('Examples') }}</span>
+        <div
+          class="flex gap-2"
+          :class="{
+            'flex-col': !wrapExamples,
+            'flex-row flex-wrap': wrapExamples,
+          }"
+        >
+          <OACodeValue
+            v-for="(example, idx) in examples"
+            :key="idx"
+            :value="example"
+          >
+            <OACodeValue />
+          </oacodevalue>
+        </div>
+      </div>
+
+      <template v-if="Object.keys(constraints).length > 0">
+        <div
+          v-for="(value, name) in constraints"
+          :key="name"
+          class="flex flex-row flex-wrap items-center gap-2"
+        >
+          <span class="text-sm">
+            {{ name }}
+          </span>
+          <OACodeValue :value="value" />
+        </div>
+      </template>
     </div>
   </div>
 </template>
diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts
index 02029f7..641f64b 100644
--- a/src/composables/useTheme.ts
+++ b/src/composables/useTheme.ts
@@ -87,6 +87,7 @@ export interface SpecConfig {
   lazyRendering?: Ref<boolean>
   defaultTag?: string
   defaultTagDescription?: string
+  wrapExamples?: boolean
 }
 
 export interface UseThemeConfig {
@@ -267,6 +268,7 @@ const themeConfig: UseThemeConfig = {
     lazyRendering: ref(false),
     defaultTag: 'Default',
     defaultTagDescription: '',
+    wrapExamples: true,
   },
   codeSamples: {
     langs: [
@@ -602,6 +604,10 @@ export function useTheme(initialConfig: Partial<UseThemeConfigUnref> = {}) {
     return themeConfig.spec
   }
 
+  function getWrapExamples() {
+    return themeConfig?.spec?.wrapExamples
+  }
+
   function setSpecConfig(config: Partial<SpecConfig>) {
     if (!themeConfig.spec) {
       themeConfig.spec = {}
@@ -639,6 +645,10 @@ export function useTheme(initialConfig: Partial<UseThemeConfigUnref> = {}) {
     if (config.defaultTagDescription !== undefined) {
       themeConfig.spec.defaultTagDescription = config.defaultTagDescription
     }
+
+    if (config.wrapExamples !== undefined) {
+      themeConfig.spec.wrapExamples = config.wrapExamples
+    }
   }
 
   function getCodeSamplesLangs() {
@@ -753,6 +763,7 @@ export function useTheme(initialConfig: Partial<UseThemeConfigUnref> = {}) {
     getI18nConfig,
     setI18nConfig,
     getSpecConfig,
+    getWrapExamples,
     setSpecConfig,
     getCodeSamplesLangs,
     getCodeSamplesDefaultLang,
diff --git a/src/lib/constraintsParser.ts b/src/lib/constraintsParser.ts
index b2d3d15..f3b716c 100644
--- a/src/lib/constraintsParser.ts
+++ b/src/lib/constraintsParser.ts
@@ -13,6 +13,7 @@ const VALIDATION_KEYWORDS: ReadonlyArray<string> = [
   'multipleOf',
   'pattern',
   'uniqueItems',
+  'default',
 ]
 
 export function hasConstraints(property: Record<string, unknown>): boolean {
diff --git a/src/lib/getExamples.ts b/src/lib/getExamples.ts
index 4bed4d0..91f3375 100644
--- a/src/lib/getExamples.ts
+++ b/src/lib/getExamples.ts
@@ -1,11 +1,27 @@
 export function getExamples(property: any): any {
+  if (property?.examples !== undefined) {
+    return property.examples
+  }
+
+  if (property?.schema?.examples !== undefined) {
+    return property.schema.examples
+  }
+
   if (property?.example !== undefined) {
     return [property.example]
   }
 
-  if (property?.examples !== undefined) {
-    return property.examples
+  if (property?.subexamples !== undefined) {
+    return property.subexamples
+  }
+
+  if (property?.schema?.example !== undefined) {
+    return [property.schema.example]
+  }
+
+  if (property?.subexample !== undefined) {
+    return [property.subexample]
   }
 
-  return undefined
+  return null
 }
diff --git a/src/locales/en.json b/src/locales/en.json
index e8a2baa..b2b900b 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -41,5 +41,6 @@
   "Operations": "Operations",
   "Default": "Default",
   "Example": "Example",
+  "Examples": "Examples",
   "tags.goTo": "View {tag} operations"
 }
diff --git a/src/locales/es.json b/src/locales/es.json
index 7e85514..ac6e6eb 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -41,5 +41,6 @@
   "Operations": "Operaciones",
   "Default": "Por defecto",
   "Example": "Ejemplo",
+  "Examples": "Ejemplos",
   "tags.goTo": "Ver operaciones de {tag}"
 }
diff --git a/src/locales/pt-BR.json b/src/locales/pt-BR.json
index f14e6d6..889ddb2 100644
--- a/src/locales/pt-BR.json
+++ b/src/locales/pt-BR.json
@@ -40,5 +40,7 @@
   "Show operations": "Exibir operações",
   "Operations": "Operações",
   "Default": "Padrão",
-  "Example": "Exemplo"
+  "Example": "Exemplo",
+  "Examples": "Exemplos",
+  "tags.goTo": "Ver operações de {tag}"
 }


Test patch:
diff --git a/test/composables/useTheme.test.ts b/test/composables/useTheme.test.ts
index 4eae78e..f7df62a 100644
--- a/test/composables/useTheme.test.ts
+++ b/test/composables/useTheme.test.ts
@@ -92,7 +92,9 @@ describe('composition API', () => {
       lazyRendering: ref(false),
       defaultTag: 'Default',
       defaultTagDescription: '',
+      wrapExamples: true,
     })
+    expect(theme.getWrapExamples()).toBe(true)
   })
 
   it('can reset', () => {
@@ -336,6 +338,7 @@ describe('useTheme', () => {
       lazyRendering: ref(false),
       defaultTag: 'Default',
       defaultTagDescription: '',
+      wrapExamples: true,
     })
   })
 
@@ -346,6 +349,7 @@ describe('useTheme', () => {
       showPathsSummary: false,
       avoidCirculars: true,
       lazyRendering: true,
+      wrapExamples: false,
     })
     const result = themeConfig.getSpecConfig()
     result.groupByTags.value = false
@@ -353,6 +357,8 @@ describe('useTheme', () => {
     result.showPathsSummary.value = false
     result.avoidCirculars.value = true
     result.lazyRendering.value = true
+    result.wrapExamples = false
+    expect(themeConfig.getWrapExamples()).toBe(false)
   })
 
   it('returns the default links prefixes config', () => {
diff --git a/test/lib/__snapshots__/processOpenAPI.test.ts.snap b/test/lib/__snapshots__/processOpenAPI.test.ts.snap
index 686c4fc..bda3d2d 100644
--- a/test/lib/__snapshots__/processOpenAPI.test.ts.snap
+++ b/test/lib/__snapshots__/processOpenAPI.test.ts.snap
@@ -23,12 +23,22 @@ exports[`processOpenAPI > processes the OpenAPI spec 1`] = `
         "required": false,
         "schema": {
           "default": 10,
+          "examples": [
+            10,
+            20,
+            50,
+          ],
           "format": "int64",
           "type": "integer",
         },
       },
       "offset": {
         "description": "The number of items to skip before starting to collect the result set",
+        "examples": [
+          1,
+          23,
+          456,
+        ],
         "in": "query",
         "name": "offset",
         "required": false,
@@ -450,12 +460,22 @@ All descriptions *can* contain ~~tons of text~~ **Markdown**. [If GitHub support
             "required": false,
             "schema": {
               "default": 10,
+              "examples": [
+                10,
+                20,
+                50,
+              ],
               "format": "int64",
               "type": "integer",
             },
           },
           {
             "description": "The number of items to skip before starting to collect the result set",
+            "examples": [
+              1,
+              23,
+              456,
+            ],
             "in": "query",
             "name": "offset",
             "required": false,
@@ -2158,12 +2178,22 @@ All descriptions *can* contain ~~tons of text~~ **Markdown**. [If GitHub support
             "required": false,
             "schema": {
               "default": 10,
+              "examples": [
+                10,
+                20,
+                50,
+              ],
               "format": "int64",
               "type": "integer",
             },
           },
           {
             "description": "The number of items to skip before starting to collect the result set",
+            "examples": [
+              1,
+              23,
+              456,
+            ],
             "in": "query",
             "name": "offset",
             "required": false,
diff --git a/test/lib/constraintsParser.test.ts b/test/lib/constraintsParser.test.ts
index a1761fa..4d35808 100644
--- a/test/lib/constraintsParser.test.ts
+++ b/test/lib/constraintsParser.test.ts
@@ -36,7 +36,7 @@ describe('getConstraints', () => {
   })
 })
 
-describe('vALIDATION_KEYWORDS', () => {
+describe('validation keywords', () => {
   it('extracts all validation keywords from property', () => {
     const property = {
       format: 'date-time',
@@ -53,6 +53,7 @@ describe('vALIDATION_KEYWORDS', () => {
       multipleOf: 2,
       pattern: '^\\d+$',
       uniqueItems: true,
+      default: 'custom',
       type: 'string', // non-validation keyword
     }
     const expectedConstraints = {
@@ -70,6 +71,7 @@ describe('vALIDATION_KEYWORDS', () => {
       multipleOf: 2,
       pattern: '^\\d+$',
       uniqueItems: true,
+      default: 'custom',
     }
     expect(getConstraints(property)).toEqual(expectedConstraints)
   })
diff --git a/test/lib/getExamples.test.ts b/test/lib/getExamples.test.ts
index ccbe54c..16e3f03 100644
--- a/test/lib/getExamples.test.ts
+++ b/test/lib/getExamples.test.ts
@@ -12,9 +12,9 @@ describe('getExamples', () => {
     expect(getExamples(property)).toEqual(['example1', 'example2'])
   })
 
-  it('returns undefined if neither example nor examples properties exist', () => {
+  it('returns null if neither example nor examples properties exist', () => {
     const property = {}
-    expect(getExamples(property)).toBeUndefined()
+    expect(getExamples(property)).toBeNull()
   })
 
   it('returns an empty array if example property is an empty string', () => {