Skip to content

Commit 6f9a5d1

Browse files
committed
Message Docs json schema
1 parent f7b7dfb commit 6f9a5d1

4 files changed

Lines changed: 1064 additions & 0 deletions

File tree

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
<!--
2+
- Open Bank Project - API Explorer II
3+
- Copyright (C) 2023-2024, TESOBE GmbH
4+
-
5+
- This program is free software: you can redistribute it and/or modify
6+
- it under the terms of the GNU Affero General Public License as published by
7+
- the Free Software Foundation, either version 3 of the License, or
8+
- (at your option) any later version.
9+
-
10+
- This program is distributed in the hope that it will be useful,
11+
- but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
- GNU Affero General Public License for more details.
14+
-
15+
- You should have received a copy of the GNU Affero General Public License
16+
- along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
-
18+
- Email: contact@tesobe.com
19+
- TESOBE GmbH
20+
- Osloerstrasse 16/17
21+
- Berlin 13359, Germany
22+
-
23+
- This product includes software developed at
24+
- TESOBE (http://www.tesobe.com/)
25+
-
26+
-->
27+
28+
<script setup lang="ts">
29+
import { ref, computed } from 'vue'
30+
import { DocumentCopy, Check } from '@element-plus/icons-vue'
31+
32+
interface Props {
33+
schema: any
34+
copyable?: boolean
35+
}
36+
37+
const props = withDefaults(defineProps<Props>(), {
38+
copyable: false
39+
})
40+
41+
const emit = defineEmits<{
42+
refClick: [refId: string]
43+
}>()
44+
45+
const copied = ref(false)
46+
47+
const copyToClipboard = async () => {
48+
try {
49+
const text = JSON.stringify(props.schema, null, 2)
50+
await navigator.clipboard.writeText(text)
51+
copied.value = true
52+
setTimeout(() => {
53+
copied.value = false
54+
}, 2000)
55+
} catch (err) {
56+
console.error('Failed to copy: ', err)
57+
}
58+
}
59+
60+
const handleRefClick = (event: Event, refId: string) => {
61+
event.preventDefault()
62+
emit('refClick', refId)
63+
}
64+
65+
// Function to render JSON with clickable $ref links
66+
const renderJsonWithRefs = (obj: any, indent: number = 0): any[] => {
67+
const result: any[] = []
68+
const indentStr = ' '.repeat(indent)
69+
70+
if (obj === null || obj === undefined) {
71+
result.push({ type: 'value', text: String(obj) })
72+
return result
73+
}
74+
75+
if (typeof obj !== 'object') {
76+
const valueClass = typeof obj === 'string' ? 'json-string' :
77+
typeof obj === 'number' ? 'json-number' :
78+
typeof obj === 'boolean' ? 'json-boolean' : 'json-value'
79+
const displayValue = typeof obj === 'string' ? `"${obj}"` : String(obj)
80+
result.push({ type: 'value', text: displayValue, class: valueClass })
81+
return result
82+
}
83+
84+
const isArray = Array.isArray(obj)
85+
const openBracket = isArray ? '[' : '{'
86+
const closeBracket = isArray ? ']' : '}'
87+
88+
result.push({ type: 'bracket', text: openBracket })
89+
90+
const entries = isArray ? obj.map((val, idx) => [idx, val]) : Object.entries(obj)
91+
const totalEntries = entries.length
92+
93+
entries.forEach(([key, value], index) => {
94+
const isLast = index === totalEntries - 1
95+
const newIndent = indent + 1
96+
const newIndentStr = ' '.repeat(newIndent)
97+
98+
result.push({ type: 'newline', text: '\n' })
99+
result.push({ type: 'indent', text: newIndentStr })
100+
101+
// Add key for objects
102+
if (!isArray) {
103+
result.push({ type: 'key', text: `"${key}"`, class: 'json-key' })
104+
result.push({ type: 'separator', text: ': ' })
105+
}
106+
107+
// Check if this is a $ref
108+
if (key === '$ref' && typeof value === 'string') {
109+
// Extract definition name from $ref
110+
const refMatch = value.match(/#\/definitions\/(.+)$/)
111+
if (refMatch) {
112+
const defName = refMatch[1]
113+
result.push({ type: 'ref', text: `"${value}"`, href: `#def-${defName}`, defName })
114+
} else {
115+
result.push({ type: 'value', text: `"${value}"`, class: 'json-string' })
116+
}
117+
} else if (value && typeof value === 'object') {
118+
// Recursively render nested objects/arrays
119+
const nested = renderJsonWithRefs(value, newIndent)
120+
result.push(...nested)
121+
} else {
122+
// Render primitive values
123+
const valueClass = typeof value === 'string' ? 'json-string' :
124+
typeof value === 'number' ? 'json-number' :
125+
typeof value === 'boolean' ? 'json-boolean' :
126+
value === null ? 'json-null' : 'json-value'
127+
const displayValue = typeof value === 'string' ? `"${value}"` : String(value)
128+
result.push({ type: 'value', text: displayValue, class: valueClass })
129+
}
130+
131+
// Add comma if not last
132+
if (!isLast) {
133+
result.push({ type: 'comma', text: ',' })
134+
}
135+
})
136+
137+
if (totalEntries > 0) {
138+
result.push({ type: 'newline', text: '\n' })
139+
result.push({ type: 'indent', text: indentStr })
140+
}
141+
result.push({ type: 'bracket', text: closeBracket })
142+
143+
return result
144+
}
145+
146+
const jsonElements = computed(() => {
147+
return renderJsonWithRefs(props.schema)
148+
})
149+
</script>
150+
151+
<template>
152+
<div class="json-schema-viewer">
153+
<div v-if="copyable" class="schema-header">
154+
<button
155+
@click="copyToClipboard"
156+
class="copy-button"
157+
:class="{ 'copied': copied }"
158+
>
159+
<span v-if="!copied">
160+
<el-icon :size="10">
161+
<DocumentCopy />
162+
</el-icon>
163+
Copy
164+
</span>
165+
<span v-else>
166+
<el-icon :size="10">
167+
<Check />
168+
</el-icon>
169+
Copied!
170+
</span>
171+
</button>
172+
</div>
173+
<div class="schema-container">
174+
<pre class="schema-pre"><code class="schema-code"><template v-for="(element, index) in jsonElements" :key="index"><span
175+
v-if="element.type === 'ref'"
176+
class="json-ref"
177+
:title="`Jump to definition: ${element.defName}`"
178+
><a :href="element.href" class="ref-link" @click="handleRefClick($event, element.href)">{{ element.text }}</a></span><span
179+
v-else-if="element.type === 'key' || element.type === 'value'"
180+
:class="element.class"
181+
>{{ element.text }}</span><span v-else>{{ element.text }}</span></template></code></pre>
182+
</div>
183+
</div>
184+
</template>
185+
186+
<style scoped>
187+
.json-schema-viewer {
188+
margin: 1rem 0;
189+
border-radius: 8px;
190+
overflow: hidden;
191+
background: #1e1e1e;
192+
border: 1px solid #333;
193+
position: relative;
194+
}
195+
196+
.schema-header {
197+
background: #2d2d2d;
198+
padding: 0.5rem 1rem;
199+
border-bottom: 1px solid #333;
200+
display: flex;
201+
justify-content: flex-end;
202+
}
203+
204+
.copy-button {
205+
background: #444;
206+
border: 1px solid #666;
207+
color: #ddd;
208+
padding: 0.25rem 0.75rem;
209+
border-radius: 4px;
210+
cursor: pointer;
211+
font-size: 12px;
212+
transition: all 0.2s ease;
213+
display: flex;
214+
align-items: center;
215+
gap: 0.25rem;
216+
}
217+
218+
.copy-button:hover {
219+
background: #555;
220+
border-color: #777;
221+
}
222+
223+
.copy-button.copied {
224+
background: #4caf50;
225+
border-color: #4caf50;
226+
color: white;
227+
}
228+
229+
.schema-container {
230+
max-height: 500px;
231+
overflow-y: auto;
232+
}
233+
234+
.schema-pre {
235+
margin: 0;
236+
padding: 1.5rem;
237+
background: #1e1e1e;
238+
color: #ddd;
239+
font-family: 'Fira Code', 'Courier New', monospace;
240+
font-size: 14px;
241+
line-height: 1.5;
242+
overflow-x: auto;
243+
}
244+
245+
.schema-code {
246+
background: transparent;
247+
padding: 0;
248+
border-radius: 0;
249+
font-family: inherit;
250+
font-size: inherit;
251+
white-space: pre;
252+
}
253+
254+
/* JSON Syntax Highlighting */
255+
.json-key {
256+
color: #e06c75;
257+
font-weight: 500;
258+
}
259+
260+
.json-string {
261+
color: #98c379;
262+
}
263+
264+
.json-number {
265+
color: #d19a66;
266+
}
267+
268+
.json-boolean {
269+
color: #56b6c2;
270+
}
271+
272+
.json-null {
273+
color: #c678dd;
274+
}
275+
276+
.json-ref {
277+
position: relative;
278+
}
279+
280+
.ref-link {
281+
color: #61afef;
282+
text-decoration: underline;
283+
text-decoration-style: dotted;
284+
cursor: pointer;
285+
transition: all 0.2s ease;
286+
}
287+
288+
.ref-link:hover {
289+
color: #84c5ff;
290+
text-decoration-style: solid;
291+
background-color: rgba(97, 175, 239, 0.1);
292+
}
293+
294+
/* Custom scrollbar */
295+
.schema-container::-webkit-scrollbar {
296+
width: 8px;
297+
}
298+
299+
.schema-container::-webkit-scrollbar-track {
300+
background: #2d2d2d;
301+
}
302+
303+
.schema-container::-webkit-scrollbar-thumb {
304+
background: #555;
305+
border-radius: 4px;
306+
}
307+
308+
.schema-container::-webkit-scrollbar-thumb:hover {
309+
background: #777;
310+
}
311+
312+
.schema-pre::-webkit-scrollbar {
313+
height: 8px;
314+
}
315+
316+
.schema-pre::-webkit-scrollbar-track {
317+
background: #2d2d2d;
318+
}
319+
320+
.schema-pre::-webkit-scrollbar-thumb {
321+
background: #555;
322+
border-radius: 4px;
323+
}
324+
325+
.schema-pre::-webkit-scrollbar-thumb:hover {
326+
background: #777;
327+
}
328+
</style>

0 commit comments

Comments
 (0)