Skip to content

Commit

Permalink
Fix cURL examples' endpoints in API reference (#679)
Browse files Browse the repository at this point in the history
* Remove network warning

* Generate 1 reference file per network

* Re-generate API reference

* Re-generate API reference

* Delete unused file

* Replace Sepolia chainId in the example files

* Re-generate API reference

* Remove redundant files
  • Loading branch information
louis-md authored Jan 23, 2025
1 parent cb4e45d commit 1c05c46
Show file tree
Hide file tree
Showing 530 changed files with 193,595 additions and 232 deletions.
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

0 comments on commit 1c05c46

Please sign in to comment.