Skip to content

Commit

Permalink
feat: Add Go code samples (#92)
Browse files Browse the repository at this point in the history
* Add Go code samples

* ci: Generate code

* ci: Format code

* Fix go request and response string value samples

* ci: Generate code

* Make go responses to return json, correctly determine go package name

* Imporve readability and structure

* ci: Generate code

* ci: Format code

* Improve var name

* Format

* Add go package import

* ci: Generate code

* Improve readability

* ci: Generate code

* Fix sdk imports

* ci: Generate code

* Imporve go package name determination and nested imports logic

* ci: Format code

* Improve var names

* More renames

* Improve array property value handling

* Improve array item check

* Improve object property value handling

* Improve code readability and structure

* Improve naming

* Add comment

* Reorg function order

* Update src/lib/code-sample/go.ts

Co-authored-by: Evan Sosenko <[email protected]>

* Fix var names casing

* ci: Format code

---------

Co-authored-by: Seam Bot <[email protected]>
Co-authored-by: Evan Sosenko <[email protected]>
  • Loading branch information
3 people authored Sep 30, 2024
1 parent 090aa96 commit 1c27ac7
Show file tree
Hide file tree
Showing 4 changed files with 489 additions and 1 deletion.
167 changes: 167 additions & 0 deletions src/lib/code-sample/go.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import { pascalCase } from 'change-case'

import type { Json, NonNullJson } from 'lib/json.js'

import { createJsonResponse } from './json.js'
import type { CodeSampleDefinition, Context } from './schema.js'

const defaultGoPackageName = 'api'
const goPackageBasePath = 'github.com/seamapi/go'

export const createGoRequest = (
{ request }: CodeSampleDefinition,
_context: Context,
): string => {
const isReqWithParams = Object.keys(request.parameters).length !== 0
const goPackageName = getGoPackageName(request.path)

const goSdkImports = generateImports({
goPackageName,
isReqWithParams,
})

const requestStructName = getRequestStructName(request.path)
const formattedArgs = formatGoArgs(request.parameters)
const goSdkRequestArgs = `context.Background()${isReqWithParams ? `, ${goPackageName}.${requestStructName}(${formattedArgs})` : ''}`

const pathParts = request.path.split('/')

return `${goSdkImports}
client${pathParts.map((p) => pascalCase(p)).join('.')}(${goSdkRequestArgs})
`.trim()
}

const getGoPackageName = (path: string): string => {
if (!isPathNested(path)) {
return defaultGoPackageName
}

const firstPathPart = path.split('/').slice(1)[1]

if (firstPathPart == null) {
throw new Error(`Invalid path: missing second part in "${path}"`)
}

return firstPathPart.replace(/_/g, '')
}

const isPathNested = (path: string): boolean =>
path.split('/').slice(1).length > 2

const generateImports = ({
goPackageName,
isReqWithParams,
}: {
goPackageName: string
isReqWithParams: boolean
}): string => {
const imports: string[] = []

if (isReqWithParams) {
const defaultPackageImport = `import ${defaultGoPackageName} "${goPackageBasePath}"`
imports.push(defaultPackageImport)
}

if (goPackageName !== defaultGoPackageName && isReqWithParams) {
const nestedPackageImport = `import ${goPackageName} "${goPackageBasePath}/${goPackageName}"`
imports.push(nestedPackageImport)
}

return imports.join('\n')
}

const getRequestStructName = (path: string): string => {
const requestStructNameSuffix = 'Request'

return isPathNested(path)
? `${pascalCase(removeUntilSecondSlash(path))}${requestStructNameSuffix}`
: `${pascalCase(path)}${requestStructNameSuffix}`
}

const removeUntilSecondSlash = (str: string): string =>
str.replace(/^\/[^/]*/, '')

const formatGoArgs = (jsonParams: NonNullJson): string =>
Object.entries(jsonParams as Record<string, Json>)
.map(([paramKey, paramValue]) => {
const formattedValue = formatGoValue({ value: paramValue, key: paramKey })
return `${pascalCase(paramKey)}: ${formattedValue}`
})
.join(', ')

const formatGoValue = ({
value,
key,
}: {
value: Json
key: string
}): string => {
if (value == null) return 'nil'
if (typeof value === 'string') return `api.String("${value}")`
if (typeof value === 'boolean') return `api.Bool(${value})`
if (typeof value === 'number') return `api.Float64(${value})`

if (Array.isArray(value)) {
return formatGoArray(value, key)
}

if (typeof value === 'object') {
return formatGoObject(value, key)
}

throw new Error(`Unsupported type: ${typeof value}`)
}

const formatGoArray = (value: Json[], key: string): string => {
if (value.length === 0) {
// in Go there's no way define an empty array without specifying type
// and code samples definitions don't include the type annotations
return 'nil'
}

const formattedItems = value.map((v) => formatGoValue({ value: v, key }))
const item = value[0]
if (item == null) {
throw new Error(`Null value in response array for '${key}'`)
}

const arrayType = isPrimitiveValue(item)
? getPrimitiveTypeName(item)
: `api.${pascalCase(key)}`

return `[${value.length}]${arrayType}{${formattedItems.join(', ')}}`
}

const isPrimitiveValue = (value: Json): boolean =>
value != null && typeof value !== 'object'

const getPrimitiveTypeName = (value: Json): string => {
switch (typeof value) {
case 'string':
return 'string'
case 'number':
return 'float64'
case 'boolean':
return 'bool'
default:
throw new Error(`Unsupported type: ${typeof value}`)
}
}

const formatGoObject = (value: Record<string, Json>, key: string): string => {
if (Object.keys(value).length === 0) {
return 'struct{}{}'
}

const formattedEntries = Object.entries(value)
.map(
([objKey, val]) =>
`${pascalCase(objKey)}: ${formatGoValue({ value: val, key: objKey })}`,
)
.join(', ')

return `api.${pascalCase(key)}{${formattedEntries}}`
}

export const createGoResponse = createJsonResponse
11 changes: 10 additions & 1 deletion src/lib/code-sample/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { JsonSchema } from 'lib/json.js'

import { formatCodeRecords } from './format.js'
import { createGoRequest, createGoResponse } from './go.js'
import {
createJavascriptRequest,
createJavascriptResponse,
Expand Down Expand Up @@ -47,12 +48,13 @@ const CodeSampleSyntaxSchema = z.enum([
'php',
'ruby',
'bash',
'go',
])

export type CodeSampleSyntax = z.infer<typeof CodeSampleSyntaxSchema>

const CodeSchema = z.record(
z.enum(['javascript', 'python', 'php', 'ruby', 'seam_cli']),
z.enum(['javascript', 'python', 'php', 'ruby', 'seam_cli', 'go']),
z.object({
title: z.string().min(1),
request: z.string(),
Expand Down Expand Up @@ -118,6 +120,13 @@ export const createCodeSample = async (
request_syntax: 'bash',
response_syntax: 'json',
},
go: {
title: 'Go',
request: createGoRequest(codeSampleDefinition, context),
response: createGoResponse(codeSampleDefinition, context),
request_syntax: 'go',
response_syntax: 'json',
},
}

return {
Expand Down
Loading

0 comments on commit 1c27ac7

Please sign in to comment.