feat: convert json to interfaces (#3566)

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: nivedin <nivedinp@gmail.com>
This commit is contained in:
Rajdip Bhattacharya
2023-12-03 23:14:26 +05:30
committed by GitHub
parent 1cc845e17d
commit bb4b640e58
9 changed files with 283 additions and 64 deletions

View File

@@ -78,6 +78,7 @@
"contact_us": "Contact us",
"cookies": "Cookies",
"copy": "Copy",
"copy_interface_type": "Copy interface type",
"copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
@@ -780,6 +781,7 @@
"connection_failed": "Connection failed",
"connection_lost": "Connection lost",
"copied_to_clipboard": "Copied to clipboard",
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
"deleted": "Deleted",
"deprecated": "DEPRECATED",
"disabled": "Disabled",

View File

@@ -76,6 +76,7 @@
"postman-collection": "^4.2.0",
"process": "^0.11.10",
"qs": "^6.11.2",
"quicktype-core": "^23.0.79",
"rxjs": "^7.8.1",
"set-cookie-parser": "^2.6.0",
"set-cookie-parser-es": "^1.0.5",

View File

@@ -160,6 +160,7 @@ declare module 'vue' {
IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default']
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']

View File

@@ -25,7 +25,7 @@
:title="`${t(
'action.download_file'
)} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
:icon="downloadResponseIcon"
:icon="downloadIcon"
@click="downloadResponse"
/>
<HoppButtonSecondary
@@ -33,9 +33,41 @@
:title="`${t(
'action.copy'
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
:icon="copyResponseIcon"
@click="copyResponse(response[0].data)"
:icon="copyIcon"
@click="copyResponse"
/>
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => copyInterfaceTippyActions.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('app.copy_interface_type')"
:icon="IconMore"
/>
<template #content="{ hide }">
<div
ref="copyInterfaceTippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
v-for="(language, index) in interfaceLanguages"
:key="index"
:label="language"
:icon="
copiedInterfaceLanguage === language
? copyInterfaceIcon
: IconCopy
"
@click="runCopyInterface(language)"
/>
</div>
</template>
</tippy>
</div>
</div>
<div ref="schemaEditor" class="flex flex-1 flex-col"></div>
@@ -59,22 +91,22 @@
<script setup lang="ts">
import IconWrapText from "~icons/lucide/wrap-text"
import IconDownload from "~icons/lucide/download"
import IconCheck from "~icons/lucide/check"
import IconCopy from "~icons/lucide/copy"
import IconMore from "~icons/lucide/more-horizontal"
import { computed, reactive, ref } from "vue"
import { refAutoReset } from "@vueuse/core"
import { useCodemirror } from "@composables/codemirror"
import { copyToClipboard } from "~/helpers/utils/clipboard"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { GQLResponseEvent } from "~/helpers/graphql/connection"
import { platform } from "~/platform"
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
import {
useCopyInterface,
useCopyResponse,
useDownloadResponse,
} from "~/composables/lens-actions"
const t = useI18n()
const toast = useToast()
const props = withDefaults(
defineProps<{
@@ -101,6 +133,7 @@ const responseString = computed(() => {
})
const schemaEditor = ref<any | null>(null)
const copyInterfaceTippyActions = ref<any | null>(null)
const linewrapEnabled = ref(true)
useCodemirror(
@@ -118,55 +151,29 @@ useCodemirror(
})
)
const downloadResponseIcon = refAutoReset<
typeof IconDownload | typeof IconCheck
>(IconDownload, 1000)
const copyResponseIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
IconCopy,
1000
const { copyIcon, copyResponse } = useCopyResponse(responseString)
const { copyInterfaceIcon, copyInterface } = useCopyInterface(responseString)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"application/json",
responseString
)
const copyResponse = (str: string) => {
copyToClipboard(str)
copyResponseIcon.value = IconCheck
toast.success(`${t("state.copied_to_clipboard")}`)
}
const copiedInterfaceLanguage = ref("")
const downloadResponse = async (str: string) => {
const dataToWrite = str
const file = new Blob([dataToWrite!], { type: "application/json" })
const url = URL.createObjectURL(file)
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
URL.revokeObjectURL(url)
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/json",
suggestedFilename: filename,
filters: [
{
name: "JSON file",
extensions: ["json"],
},
],
const runCopyInterface = (language: string) => {
copyInterface(language).then(() => {
copiedInterfaceLanguage.value = language
})
if (result.type === "unknown" || result.type === "saved") {
downloadResponseIcon.value = IconCheck
toast.success(`${t("state.download_started")}`)
}
}
defineActionHandler(
"response.file.download",
() => downloadResponse(responseString.value),
() => downloadResponse(),
computed(() => !!props.response && props.response.length > 0)
)
defineActionHandler(
"response.copy",
() => copyResponse(responseString.value),
() => copyResponse(),
computed(() => !!props.response && props.response.length > 0)
)
</script>

View File

@@ -44,6 +44,39 @@
:icon="copyIcon"
@click="copyResponse"
/>
<tippy
v-if="response.body"
interactive
trigger="click"
theme="popover"
:on-shown="() => copyInterfaceTippyActions.focus()"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('app.copy_interface_type')"
:icon="IconMore"
/>
<template #content="{ hide }">
<div
ref="copyInterfaceTippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.escape="hide()"
>
<HoppSmartItem
v-for="(language, index) in interfaceLanguages"
:key="index"
:label="language"
:icon="
copiedInterfaceLanguage === language
? copyInterfaceIcon
: IconCopy
"
@click="runCopyInterface(language)"
/>
</div>
</template>
</tippy>
</div>
</div>
<div
@@ -201,7 +234,9 @@
<script setup lang="ts">
import IconWrapText from "~icons/lucide/wrap-text"
import IconFilter from "~icons/lucide/filter"
import IconMore from "~icons/lucide/more-horizontal"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconCopy from "~icons/lucide/copy"
import * as LJSON from "lossless-json"
import * as O from "fp-ts/Option"
import * as E from "fp-ts/Either"
@@ -221,9 +256,11 @@ import {
useCopyResponse,
useResponseBody,
useDownloadResponse,
useCopyInterface,
} from "@composables/lens-actions"
import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
const t = useI18n()
@@ -235,6 +272,13 @@ const { responseBodyText } = useResponseBody(props.response)
const toggleFilter = ref(false)
const filterQueryText = ref("")
const copiedInterfaceLanguage = ref("")
const runCopyInterface = (language: string) => {
copyInterface(language).then(() => {
copiedInterfaceLanguage.value = language
})
}
type BodyParseError =
| { type: "JSON_PARSE_FAILED" }
@@ -319,6 +363,7 @@ const filterResponseError = computed(() =>
)
const { copyIcon, copyResponse } = useCopyResponse(jsonBodyText)
const { copyInterfaceIcon, copyInterface } = useCopyInterface(jsonBodyText)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"application/json",
jsonBodyText
@@ -327,6 +372,7 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
// Template refs
const tippyActions = ref<any | null>(null)
const jsonResponse = ref<any | null>(null)
const copyInterfaceTippyActions = ref<any | null>(null)
const linewrapEnabled = ref(true)
const { cursor } = useCodemirror(

View File

@@ -11,6 +11,29 @@ import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "@helpers/utils/clipboard"
import { HoppRESTResponse } from "@helpers/types/HoppRESTResponse"
import { platform } from "~/platform"
import jsonToLanguage from "~/helpers/utils/json-to-language"
export function useCopyInterface(responseBodyText: Ref<string>) {
const toast = useToast()
const t = useI18n()
const copyInterfaceIcon = refAutoReset(IconCopy, 1000)
const copyInterface = async (targetLanguage: string) => {
jsonToLanguage(targetLanguage, responseBodyText.value).then((res) => {
copyToClipboard(res.lines.join("\n"))
copyInterfaceIcon.value = IconCheck
toast.success(
t("state.copied_interface_to_clipboard", { language: targetLanguage })
)
})
}
return {
copyInterfaceIcon,
copyInterface,
}
}
export function useCopyResponse(responseBodyText: Ref<any>) {
const toast = useToast()

View File

@@ -0,0 +1,26 @@
const interfaceLanguages = [
"cJSON",
"C++",
"C#",
"Crystal",
"Dart",
"Elm",
"Flow",
"Go",
"Haskell",
"Java",
"JavaScript",
"Kotlin",
"Objective-C",
"PHP",
"Pike",
"Python",
"Ruby",
"Rust",
"Scala3",
"Smithy",
"Swift",
"TypeScript",
]
export default interfaceLanguages

View File

@@ -0,0 +1,27 @@
import {
quicktype,
InputData,
jsonInputForTargetLanguage,
} from "quicktype-core"
async function jsonToLanguage(targetLanguage: string, jsonString: string) {
const jsonInput = jsonInputForTargetLanguage(targetLanguage)
await jsonInput.addSource({
name: "JSONSchema",
samples: [jsonString],
})
const inputData = new InputData()
inputData.addInput(jsonInput)
return await quicktype({
inputData,
lang: targetLanguage,
rendererOptions: {
"just-types": true,
},
})
}
export default jsonToLanguage