Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix cURL examples' endpoints in API reference #679

Merged
merged 8 commits into from
Jan 23, 2025
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
139 changes: 86 additions & 53 deletions .github/scripts/generateApiReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ const fs = require('fs')
const { capitalize } = require('lodash')
const YAML = require('yaml')

const jsonFile = require('../../components/ApiReference/mainnet-swagger.json')
const pathsMetadata = require('../../components/ApiReference/paths-metadata.json')
const txServiceNetworks = require('../../components/ApiReference/tx-service-networks.json')

const baseUrl = 'https://safe-transaction-sepolia.safe.global'

const curlify = (req: any) =>
`curl -X ${req.method} https://safe-transaction-sepolia.safe.global/api${
`curl -X ${req.method} https://safe-transaction-${req.networkName}.safe.global/api${
req.url
} \\
-H "Accept: application/json" \\
Expand Down Expand Up @@ -147,11 +144,13 @@ const generateSampleApiResponse = async (
path: string,
pathWithParams: string,
method: string,
requestBody: string
requestBody: string,
networkName: string
) => {
const fetch = await import('node-fetch')

let response: any
const baseUrl = `https://safe-transaction-${networkName}.safe.global`
const url = baseUrl + pathWithParams
if (method === 'get') {
response = await fetch.default(url).then(async res => {
Expand Down Expand Up @@ -200,26 +199,24 @@ const generateSampleApiResponse = async (
}

const slugify = (text: string) => text?.replace?.(/ /g, '-').replace(/\//g, '-')
const resolveRef = (ref: string) => {
const resolveRef = (swagger: any, ref: string) => {
const refName = ref.split('/').pop()
return { refName, ...jsonFile.components.schemas[refName as string] }
return { refName, ...swagger.components.schemas[refName as string] }
}

const resolveRefs = (obj: any) => {
const resolveRefs = (swagger: any, obj: any) => {
if (typeof obj === 'object') {
for (const key in obj) {
if (key === '$ref') {
obj = resolveRef(obj[key])
obj = resolveRef(swagger, obj[key])
} else {
obj[key] = resolveRefs(obj[key])
obj[key] = resolveRefs(swagger, obj[key])
}
}
}
return obj
}

const mainnetApiJson = resolveRefs(jsonFile)

const addMethodContext = (json: any) => ({
...json,
paths: Object.entries(json.paths).reduce((acc, [path, methods]) => {
Expand All @@ -243,31 +240,36 @@ const addMethodContext = (json: any) => ({
}, {})
})

const getApiJson = async (url: string) => {
const getApiJson = async (url: string, networkName: string) => {
const response = await fetch(url + '/schema/')
const yaml = await response.text()
const json = YAML.parse(yaml)
const withContext = addMethodContext(json)
fs.writeFileSync(
'./components/ApiReference/mainnet-swagger.json',
`./components/ApiReference/schemas/${networkName}-swagger.json`,
JSON.stringify(withContext, null, 2)
)
return withContext
}

const generateMethodContent = (path: string, method: string) => {
const _method = mainnetApiJson.paths[path][method]
const generateMethodContent = (
swagger: any,
networkName: string,
path: string,
method: string
) => {
const _method = swagger.paths[path][method]
const responses = Object.entries(_method.responses).map(
([code, { schema, ...data }]: [any, any]) => ({
code,
schema:
schema?.['$ref'] !== undefined
? resolveRef(schema['$ref'])
? resolveRef(swagger, schema['$ref'])
: {
...schema,
items:
schema?.items?.['$ref'] !== undefined
? resolveRef(schema.items['$ref'])
? resolveRef(swagger, schema.items['$ref'])
: schema?.items
},
...data
Expand All @@ -289,7 +291,8 @@ const generateMethodContent = (path: string, method: string) => {
const filePath = `./components/ApiReference/examples/${slugify(
path
)}-${method}`.replace('-api', '')
const examplePath = filePath + '.ts'
const examplePath =
filePath.replace('examples', `examples/${networkName}`) + '.ts'
const sampleResponsePath = filePath + '.json'
const hasExample = fs.existsSync(examplePath)
const hasResponse = fs.existsSync(sampleResponsePath)
Expand Down Expand Up @@ -317,7 +320,7 @@ const generateMethodContent = (path: string, method: string) => {

// This is commented out, as we omit response generation for now.
// It is planned to move this into a separate script.
// generateSampleApiResponse(path, pathWithParams + query, method, requestBody)
// generateSampleApiResponse(path, pathWithParams + query, method, requestBody, networkName)

const codeBlockWithinDescription = _method.description?.match(
/```[a-z]*\n[\s\S]*?\n```/
Expand Down Expand Up @@ -357,7 +360,7 @@ ${
hasExample && example !== 'export {}\n'
? `
\`\`\`js TypeScript
// from ${examplePath.replace('./components/ApiReference/', '')}
// from ${examplePath.replace('./components/ApiReference/', '../')}
\`\`\`
`
: ''
Expand All @@ -366,12 +369,12 @@ ${
${curlify({
url: pathWithParams,
method: method.toUpperCase(),
body: requestBody
body: requestBody,
networkName
})}
\`\`\`
</CH.Code>
</CH.Section>
<NetworkNotice />

${
hasResponse && sampleResponse !== '{}'
Expand All @@ -391,25 +394,29 @@ ${sampleResponse}
`
}

const generatePathContent = (path: string) =>
`${Object.keys(mainnetApiJson.paths[path])
const generatePathContent = (swagger: any, networkName: string, path: string) =>
`${Object.keys(swagger.paths[path])
.filter(method => method !== 'parameters')
.map(method => generateMethodContent(path, method))
.map(method => generateMethodContent(swagger, networkName, path, method))
.join('\n')}`

const generateCategoryContent = (category: {
title: string
paths: string[]
}) => `<Grid my={8} />
const generateCategoryContent = (
swagger: any,
networkName: string,
category: {
title: string
paths: string[]
}
) => `<Grid my={8} />

## ${capitalize(category.title)}

<Grid my={6} />

${category.paths.map(path => generatePathContent(path)).join('\n')}`
${category.paths.map(path => generatePathContent(swagger, networkName, path)).join('\n')}`

const getCategories = () => {
const allMethods: any = Object.entries(mainnetApiJson.paths)
const getCategories = (swagger: any) => {
const allMethods: any = Object.entries(swagger.paths)
.map(([k, v]: [any, any]) => Object.values(v))
.flat()
const allCategories = Array.from(
Expand All @@ -432,18 +439,17 @@ const getCategories = () => {
}))
}

const generateMainContent = () => {
const categories = getCategories().filter(
const generateMainContent = (swagger: any, networkName: string) => {
const categories = getCategories(swagger).filter(
c => c.title !== 'about' && c.title !== 'notifications'
)

return `import Path from './Path'
import Hr from '../Hr'
import SampleRequestHeader from './SampleRequestHeader'
import Parameters from './Parameter'
import NetworkSwitcher, { NetworkNotice } from './Network'
import Responses from './Response'
import Feedback from '../Feedback'
return `import Path from '../Path'
import Hr from '../../Hr'
import SampleRequestHeader from '../SampleRequestHeader'
import Parameters from '../Parameter'
import NetworkSwitcher from '../Network'
import Responses from '../Response'
import Feedback from '../../Feedback'
import Grid from '@mui/material/Grid'
import Box from '@mui/material/Box'
import NextLink from 'next/link'
Expand All @@ -453,34 +459,39 @@ import Link from '@mui/material/Link'

The Safe Transaction Service API Reference is a collection of endpoints that allow to keep track of Safe transactions.

This service is available on [multiple networks](../../core-api/transaction-service-supported-networks), at different endpoints.
This service is available on [multiple networks](../../../core-api/transaction-service-supported-networks), at different endpoints.

<NetworkSwitcher />

${categories.map(category => generateCategoryContent(category)).join('\n')}
${categories.map(category => generateCategoryContent(swagger, networkName, category)).join('\n')}
`
}

const main = async () => {
await getApiJson('https://safe-transaction-mainnet.safe.global')
txServiceNetworks.forEach(
async (network: { chainId: string; txServiceUrl: string }) => {
const networkName = network.txServiceUrl
.replace('https://safe-transaction-', '')
.split('.')[0]
// Download swagger schema and converts it from YAML to JSON.
const jsonFile = await getApiJson(network.txServiceUrl, networkName)
const resolvedJson = resolveRefs(jsonFile, jsonFile)

// Generate the page which will load the reference file, and parse it to generate the dynamic sidebar on the client side.
fs.writeFileSync(
`./pages/core-api/transaction-service-reference/${networkName}.mdx`,
`
{/* <!-- vale off --> */}
import ApiReference from '../../../components/ApiReference'
import { renderToString } from 'react-dom/server'
import { MDXComponents, getHeadingsFromHtml } from '../../../lib/mdx'
import Mdx from '../../../components/ApiReference/generated-reference.mdx'
import Mdx from '../../../components/ApiReference/generated/${networkName}-reference.mdx'
import swagger from '../../../components/ApiReference/schemas/${networkName}-swagger.json'

export const getStaticProps = async () => {
const renderedMdx = <Mdx components={MDXComponents} />
const contentString = renderToString(renderedMdx)
const headings = getHeadingsFromHtml(contentString)
const headings = getHeadingsFromHtml(swagger, contentString)

return {
props: {
Expand All @@ -493,13 +504,35 @@ export const getStaticProps = async () => {
{/* <!-- vale on --> */}
`
)

// Generate the main reference file.
const mdxContent = generateMainContent(resolvedJson, networkName)
fs.writeFileSync(
`./components/ApiReference/generated/${networkName}-reference.mdx`,
mdxContent
)

// Replace Sepolia chainId in the example files.
const exampleFiles = fs
.readdirSync('./components/ApiReference/examples/sepolia')
.filter((file: string) => file.endsWith('.ts'))
exampleFiles.forEach((file: string) => {
const contents = fs.readFileSync(
`./components/ApiReference/examples/${file}`,
'utf-8'
)
if (
!fs.existsSync(`./components/ApiReference/examples/${networkName}`)
) {
fs.mkdirSync(`./components/ApiReference/examples/${networkName}`)
}
fs.writeFileSync(
`./components/ApiReference/examples/${networkName}/${file}`,
contents.replace('chainId: 11155111n', `chainId: ${network.chainId}n`)
)
})
}
)
const mdxContent = generateMainContent()
fs.writeFileSync(
`./components/ApiReference/generated-reference.mdx`,
mdxContent
)
}

main()
18 changes: 14 additions & 4 deletions components/ApiReference/ApiReference.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { useData } from 'nextra/ssg'
import Grid from '@mui/material/Grid'
import Dialog from '@mui/material/Dialog'
Expand All @@ -14,17 +14,27 @@ import ExpandLess from '@mui/icons-material/ExpandLess'

import TOC, { type Heading } from './TOC'
import { MDXComponents, useCurrentTocIndex } from '../../lib/mdx'
import Mdx from './generated-reference.mdx'
import { NetworkProvider } from './Network'
import css from './styles.module.css'

const renderedMdx = <Mdx components={MDXComponents} />

const ApiReference: React.FC<{ networkName: string }> = ({ networkName }) => {
const { headings } = useData()
const [Mdx, setMdx] = useState<React.ComponentType<{
components: typeof MDXComponents
}> | null>(null)
const renderedMdx = Mdx != null ? <Mdx components={MDXComponents} /> : null
const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false)
const currentIndex = useCurrentTocIndex(headings as Heading[], 100)

useEffect(() => {
void (async () => {
const { default: Component } = await import(
`./generated/${networkName}-reference.mdx`
)
setMdx(() => Component)
})()
}, [networkName])

return (
<>
<Grid container justifyContent='space-between'>
Expand Down
39 changes: 0 additions & 39 deletions components/ApiReference/Network.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,14 @@ import {
useContext
} from 'react'
import Link from 'next/link'
import MuiLink from '@mui/material/Link'
import Select from '@mui/material/Select'
import Box from '@mui/material/Box'
import MenuItem from '@mui/material/MenuItem'
import Grid from '@mui/material/Grid'
import Typography from '@mui/material/Typography'
import Button from '@mui/material/Button'
import GetAppIcon from '@mui/icons-material/GetApp'
import { capitalize } from 'lodash'
import { CopyToClipboard } from 'nextra/components'
import Check from '@mui/icons-material/Check'

import txServiceNetworks from './tx-service-networks.json'

Expand Down Expand Up @@ -143,40 +140,4 @@ const NetworkSwitcher: React.FC = () => {
)
}

export const NetworkNotice: React.FC = () => {
const [network] = useContext(NetworkContext)
const [copied, setCopied] = useState(false)
return (
network !== transactionServiceUrls[indexOfDefaultNetwork] && (
<Box sx={{ fontSize: '12px', mt: -2, mb: 3 }}>
This snippet shows a sample request on Ethereum Sepolia. Please{' '}
<MuiLink
sx={{ '&:hover': { cursor: 'pointer' } }}
onClick={() => {
void navigator.clipboard.writeText(network)
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 3000)
}}
>
click here
</MuiLink>{' '}
<Check
sx={{
fontSize: '12px',
width: copied ? '12px' : 0,
opacity: copied ? 1 : 0,
mr: copied ? 0.5 : 0,
transition: '0.4s'
}}
/>
to copy the base URL for{' '}
{capitalize(network?.split('-')[2]?.split('.')[0])} and update it in
your request.
</Box>
)
)
}

export default NetworkSwitcher
Loading
Loading