Repository: enzonotario/vitepress-openapi (ts)

Issue:
Ugly display of { and } in example code
The generated example code looks like this...

```
curl 'https://api.foo/v2/devices/%7Bid%7D/tags' \
  --header 'Authorization: xxx'
```

However really in the UI it would be nicer to look like

```
curl 'https://api.foo/v2/devices/{id}/tags' \
  --header 'Authorization: xxx'
```

Pull request description:
feat(request): decode URL strings for better Code Samples
<!-- Provide a general summary of your changes in the title above. -->

# Description

Decodes previously encoded URLs for better displaying.

## Related issues/external references

Closes #223 

## Types of changes

- New feature


Patch:
diff --git a/src/lib/codeSamples/buildHarRequest.ts b/src/lib/codeSamples/buildHarRequest.ts
index 49b2b12..28430f8 100644
--- a/src/lib/codeSamples/buildHarRequest.ts
+++ b/src/lib/codeSamples/buildHarRequest.ts
@@ -6,7 +6,7 @@ export function buildHarRequest(
 ): HarRequest {
   const harRequest: HarRequest = {
     method: oaRequest.method.toUpperCase(),
-    url: oaRequest.url.toString(),
+    url: decodeURI(oaRequest.url.toString()),
     httpVersion: 'HTTP/1.1',
     headers: Object.entries(oaRequest.headers).map(([name, value]) => ({
       name: name.replace(/\b\w/g, letter => letter.toUpperCase()), // Convert to title case.
diff --git a/src/lib/codeSamples/buildRequest.ts b/src/lib/codeSamples/buildRequest.ts
index fffe64d..f7b1977 100644
--- a/src/lib/codeSamples/buildRequest.ts
+++ b/src/lib/codeSamples/buildRequest.ts
@@ -134,6 +134,7 @@ export function buildRequest({
   body = undefined,
   headers = undefined,
   variables = {},
+  cookies = {},
 }: Partial<OARequest>): OARequest {
   const resolvedVariables = setExamplesAsVariables(parameters, variables)
 
@@ -194,5 +195,7 @@ export function buildRequest({
     variables: resolvedVariables,
     headers: resolvedHeaders,
     query: resolvedQuery,
+    cookies,
+    contentType: resolvedHeaders['content-type'],
   })
 }


Test patch:
diff --git a/test/lib/buildRequest.test.ts b/test/lib/buildRequest.test.ts
index 623fc2b..026ed31 100644
--- a/test/lib/buildRequest.test.ts
+++ b/test/lib/buildRequest.test.ts
@@ -223,6 +223,31 @@ describe('buildRequest', () => {
   })
 })
 
+describe('cookies', () => {
+  it('builds request with cookies', () => {
+    const request = buildRequest({
+      path: '/users',
+      method: 'GET',
+      baseUrl: 'https://api.example.com',
+      parameters: [],
+      cookies: { session: 'abc123' },
+    })
+    expect(request.cookies.session).toBe('abc123')
+  })
+
+  it('builds request with multiple cookies', () => {
+    const request = buildRequest({
+      path: '/users',
+      method: 'GET',
+      baseUrl: 'https://api.example.com',
+      parameters: [],
+      cookies: { session: 'abc123', user: 'john' },
+    })
+    expect(request.cookies.session).toBe('abc123')
+    expect(request.cookies.user).toBe('john')
+  })
+})
+
 describe('update request', () => {
   it('updates request with new headers', () => {
     const request = buildRequest({
@@ -345,4 +370,21 @@ describe('update request', () => {
 
     expect(fixedRequest.query.search).toBeUndefined()
   })
+
+  it('updates request with new cookies', () => {
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/resource',
+      method: 'GET',
+      cookies: { session: 'old-session' },
+    })
+
+    const fixedRequest = buildRequest({
+      ...request,
+      cookies: { session: 'new-session', user: 'john' },
+    })
+
+    expect(fixedRequest.cookies.session).toBe('new-session')
+    expect(fixedRequest.cookies.user).toBe('john')
+  })
 })
diff --git a/test/lib/codeSamples/buildHarRequest.test.ts b/test/lib/codeSamples/buildHarRequest.test.ts
new file mode 100644
index 0000000..e4953e3
--- /dev/null
+++ b/test/lib/codeSamples/buildHarRequest.test.ts
@@ -0,0 +1,342 @@
+import { describe, expect, it } from 'vitest'
+import { buildHarRequest } from '../../../src/lib/codeSamples/buildHarRequest'
+import { buildRequest } from '../../../src/lib/codeSamples/buildRequest'
+
+describe('buildHarRequest', () => {
+  it('generates HAR request for basic GET request', () => {
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/resource',
+      method: 'GET',
+    })
+    const result = buildHarRequest(request)
+
+    expect(result).toEqual({
+      method: 'GET',
+      url: 'https://api.example.com/resource',
+      httpVersion: 'HTTP/1.1',
+      headers: [],
+      queryString: [],
+      cookies: [],
+      headersSize: -1,
+      bodySize: -1,
+    })
+  })
+
+  it('generates HAR request with headers', () => {
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/resource',
+      method: 'GET',
+      headers: { 'Authorization': 'Bearer token', 'Content-Type': 'application/json' },
+    })
+    const result = buildHarRequest(request)
+
+    expect(result).toEqual({
+      method: 'GET',
+      url: 'https://api.example.com/resource',
+      httpVersion: 'HTTP/1.1',
+      headers: [
+        { name: 'Authorization', value: 'Bearer token' },
+        { name: 'Content-Type', value: 'application/json' },
+      ],
+      queryString: [],
+      cookies: [],
+      headersSize: -1,
+      bodySize: -1,
+    })
+  })
+
+  it('generates HAR request with query parameters', () => {
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/resource',
+      method: 'GET',
+      parameters: [
+        { name: 'search', in: 'query' },
+        { name: 'page', in: 'query' },
+      ],
+      variables: {
+        search: 'query',
+        page: '2',
+      },
+    })
+    const result = buildHarRequest(request)
+
+    expect(result).toEqual({
+      method: 'GET',
+      url: 'https://api.example.com/resource',
+      httpVersion: 'HTTP/1.1',
+      headers: [],
+      queryString: [
+        { name: 'search', value: 'query' },
+        { name: 'page', value: '2' },
+      ],
+      cookies: [],
+      headersSize: -1,
+      bodySize: -1,
+    })
+  })
+
+  it('generates HAR request with cookies', () => {
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/resource',
+      method: 'GET',
+      cookies: { session: 'abc123', user: 'john' },
+    })
+
+    const result = buildHarRequest(request)
+
+    expect(result).toEqual({
+      method: 'GET',
+      url: 'https://api.example.com/resource',
+      httpVersion: 'HTTP/1.1',
+      headers: [],
+      queryString: [],
+      cookies: [
+        { name: 'session', value: 'abc123' },
+        { name: 'user', value: 'john' },
+      ],
+      headersSize: -1,
+      bodySize: -1,
+    })
+  })
+
+  it('generates HAR request with string body', () => {
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/resource',
+      method: 'POST',
+      body: 'plain text body',
+      headers: { 'Content-Type': 'text/plain' },
+    })
+    const result = buildHarRequest(request)
+
+    expect(result).toEqual({
+      method: 'POST',
+      url: 'https://api.example.com/resource',
+      httpVersion: 'HTTP/1.1',
+      headers: [
+        { name: 'Content-Type', value: 'text/plain' },
+      ],
+      queryString: [],
+      cookies: [],
+      headersSize: -1,
+      bodySize: -1,
+      postData: {
+        mimeType: 'text/plain',
+        text: 'plain text body',
+      },
+    })
+  })
+
+  it('generates HAR request with JSON object body', () => {
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/resource',
+      method: 'POST',
+      body: { key: 'value', nested: { prop: 'nestedValue' } },
+    })
+    const result = buildHarRequest(request)
+
+    expect(result).toEqual({
+      method: 'POST',
+      url: 'https://api.example.com/resource',
+      httpVersion: 'HTTP/1.1',
+      headers: [
+        { name: 'Content-Type', value: 'application/json' },
+      ],
+      queryString: [],
+      cookies: [],
+      headersSize: -1,
+      bodySize: -1,
+      postData: {
+        mimeType: 'application/json',
+        text: JSON.stringify({ key: 'value', nested: { prop: 'nestedValue' } }),
+      },
+    })
+  })
+
+  it('generates HAR request with FormData body', () => {
+    const formData = new FormData()
+    formData.append('text', 'text value')
+    formData.append('number', '123')
+
+    const file = new File(['file content'], 'test.txt', { type: 'text/plain' })
+    formData.append('file', file)
+
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/upload',
+      method: 'POST',
+      body: formData,
+      parameters: [
+        { name: 'Content-Type', in: 'header' },
+      ],
+      variables: {
+        'Content-Type': 'multipart/form-data',
+      },
+    })
+
+    const result = buildHarRequest(request)
+
+    const expectedFormDataObject = {
+      text: 'text value',
+      number: '123',
+      file: {
+        type: 'file',
+        text: 'BINARY',
+        name: 'test.txt',
+        size: file.size,
+        mimeType: 'text/plain',
+      },
+    }
+
+    expect(result).toEqual({
+      method: 'POST',
+      url: 'https://api.example.com/upload',
+      httpVersion: 'HTTP/1.1',
+      headers: [
+        { name: 'Content-Type', value: 'multipart/form-data' },
+      ],
+      queryString: [],
+      cookies: [],
+      headersSize: -1,
+      bodySize: -1,
+      postData: {
+        mimeType: 'multipart/form-data',
+        text: JSON.stringify(expectedFormDataObject),
+      },
+    })
+  })
+
+  it('generates HAR request with multiple values for the same FormData key', () => {
+    const formData = new FormData()
+    formData.append('tags', 'tag1')
+    formData.append('tags', 'tag2')
+    formData.append('tags', 'tag3')
+    formData.append('single', 'value')
+
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/upload',
+      method: 'POST',
+      body: formData,
+      parameters: [
+        { name: 'Content-Type', in: 'header' },
+      ],
+      variables: {
+        'Content-Type': 'multipart/form-data',
+      },
+    })
+
+    const result = buildHarRequest(request)
+
+    const expectedFormDataObject = {
+      tags: ['tag1', 'tag2', 'tag3'],
+      single: 'value',
+    }
+
+    expect(result).toEqual({
+      method: 'POST',
+      url: 'https://api.example.com/upload',
+      httpVersion: 'HTTP/1.1',
+      headers: [
+        { name: 'Content-Type', value: 'multipart/form-data' },
+      ],
+      queryString: [],
+      cookies: [],
+      headersSize: -1,
+      bodySize: -1,
+      postData: {
+        mimeType: 'multipart/form-data',
+        text: JSON.stringify(expectedFormDataObject),
+      },
+    })
+  })
+
+  it('generates HAR request with all parameters combined', () => {
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/resource',
+      method: 'PUT',
+      headers: { 'Authorization': 'Bearer token', 'Content-Type': 'application/json' },
+      parameters: [
+        { name: 'search', in: 'query' },
+        { name: 'page', in: 'query' },
+      ],
+      variables: {
+        search: 'query',
+        page: '2',
+      },
+      body: { key: 'value', nested: { prop: 'nestedValue' } },
+      cookies: { session: 'abc123' },
+    })
+
+    const result = buildHarRequest(request)
+
+    expect(result).toEqual({
+      method: 'PUT',
+      url: 'https://api.example.com/resource',
+      httpVersion: 'HTTP/1.1',
+      headers: [
+        { name: 'Authorization', value: 'Bearer token' },
+        { name: 'Content-Type', value: 'application/json' },
+      ],
+      queryString: [
+        { name: 'search', value: 'query' },
+        { name: 'page', value: '2' },
+      ],
+      cookies: [
+        { name: 'session', value: 'abc123' },
+      ],
+      headersSize: -1,
+      bodySize: -1,
+      postData: {
+        mimeType: 'application/json',
+        text: JSON.stringify({ key: 'value', nested: { prop: 'nestedValue' } }),
+      },
+    })
+  })
+
+  it('decodes URL encoded strings', () => {
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/users/{id}',
+      method: 'GET',
+    })
+
+    const result = buildHarRequest(request)
+
+    expect(result).toEqual({
+      method: 'GET',
+      url: 'https://api.example.com/users/{id}',
+      httpVersion: 'HTTP/1.1',
+      headers: [],
+      queryString: [],
+      cookies: [],
+      headersSize: -1,
+      bodySize: -1,
+    })
+  })
+
+  it('decodes percent-encoded spaces and Unicode chars', () => {
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/files/Space%20Name%20%C3%87har',
+      method: 'GET',
+    })
+    const result = buildHarRequest(request)
+    expect(result).toEqual({
+      method: 'GET',
+      url: 'https://api.example.com/files/Space Name Çhar',
+      httpVersion: 'HTTP/1.1',
+      headers: [],
+      queryString: [],
+      cookies: [],
+      headersSize: -1,
+      bodySize: -1,
+    })
+  })
+})
diff --git a/test/lib/codeSamples/generateCodeSamples.test.ts b/test/lib/codeSamples/generateCodeSamples.test.ts
index 7239aac..4e07a4d 100644
--- a/test/lib/codeSamples/generateCodeSamples.test.ts
+++ b/test/lib/codeSamples/generateCodeSamples.test.ts
@@ -220,6 +220,16 @@ describe('curl', () => {
   --header 'Authorization: Bearer token' \\
   --header 'Content-Type: application/json'`)
   })
+
+  it('displays encoded URLs correctly', async () => {
+    const request = buildRequest({
+      baseUrl: 'https://api.example.com',
+      path: '/users/{userId}/posts',
+      method: 'GET',
+    })
+    const result = await generateCodeSample('curl', request)
+    expect(result).toBe(`curl 'https://api.example.com/users/{userId}/posts'`)
+  })
 })
 
 describe('php', () => {