diff --git a/.github/scripts/fern-scribe.js b/.github/scripts/fern-scribe.js index 845038fdf..de11424d6 100644 --- a/.github/scripts/fern-scribe.js +++ b/.github/scripts/fern-scribe.js @@ -3,7 +3,106 @@ const Turbopuffer = require('@turbopuffer/turbopuffer').default; const fs = require('fs').promises; const path = require('path'); const yaml = require('js-yaml'); +const https = require('https'); +const http = require('http'); const FernUrlMapper = require('./fern-url-mapper'); +const fsSync = require('fs'); // For synchronous read + +// Parse the Product Root Directories section from my-mappings.md +function parseProductRootMapping(mappingsPath = path.join(__dirname, 'my-mappings.md')) { + const slugToDir = {}; + if (!fsSync.existsSync(mappingsPath)) return slugToDir; + const content = fsSync.readFileSync(mappingsPath, 'utf-8'); + const rootSection = content.split('## Product Root Directories')[1]?.split('##')[0] || ''; + rootSection.split('\n').forEach(line => { + const match = line.match(/^([\w-]+):\s*([\w-]+)/); + if (match) { + slugToDir[match[1].trim()] = match[2].trim(); + } + }); + return slugToDir; +} + +// Helper to parse my-mappings.md and build slug->dir mapping +function buildProductSlugToDirMap(mappingsPath = path.join(__dirname, 'my-mappings.md')) { + const slugToDir = {}; + if (!fsSync.existsSync(mappingsPath)) return slugToDir; + const content = fsSync.readFileSync(mappingsPath, 'utf-8'); + // Regex: /learn/... → fern/products//pages + const regex = /\/learn\/([\w-]+)[^`]*?→\s*fern\/products\/([\w-]+)\/pages/g; + let match; + while ((match = regex.exec(content)) !== null) { + const slug = match[1]; + const dir = match[2]; + slugToDir[slug] = dir; + } + return slugToDir; +} + +// Parse all /learn/... → file path mappings from my-mappings.md +function parseLearnToFileMapping(mappingsPath = path.join(__dirname, 'my-mappings.md')) { + const learnToFile = {}; + if (!fsSync.existsSync(mappingsPath)) return learnToFile; + const content = fsSync.readFileSync(mappingsPath, 'utf-8'); + const mappingLines = content.split('\n').filter(line => line.trim().startsWith('- `')); + for (const line of mappingLines) { + const match = line.match(/- `([^`]+)` → `([^`]+)`/); + if (match) { + learnToFile[match[1].trim()] = match[2].trim(); + } + } + return learnToFile; +} + +// Helper function to replace fetch with Node.js built-in modules +function httpRequest(url, options = {}) { + return new Promise((resolve, reject) => { + const urlObj = new URL(url); + const isHttps = urlObj.protocol === 'https:'; + const lib = isHttps ? https : http; + + const requestOptions = { + hostname: urlObj.hostname, + port: urlObj.port || (isHttps ? 443 : 80), + path: urlObj.pathname + urlObj.search, + method: options.method || 'GET', + headers: options.headers || {}, + }; + + const req = lib.request(requestOptions, (res) => { + const chunks = []; + + res.on('data', chunk => { + chunks.push(chunk); + }); + + res.on('end', () => { + const buffer = Buffer.concat(chunks); + const data = buffer.toString('utf8'); + + const response = { + ok: res.statusCode >= 200 && res.statusCode < 300, + status: res.statusCode, + headers: { + get: (name) => res.headers[name.toLowerCase()] + }, + json: () => Promise.resolve(JSON.parse(data)), + text: () => Promise.resolve(data), + arrayBuffer: () => Promise.resolve(buffer) + }; + resolve(response); + }); + }); + + req.on('error', reject); + + if (options.body) { + req.write(options.body); + } + + req.end(); + }); +} class FernScribeGitHub { constructor() { @@ -25,6 +124,11 @@ class FernScribeGitHub { // Use centralized URL mapper this.urlMapper = new FernUrlMapper(process.env.GITHUB_TOKEN, process.env.REPOSITORY); + this.productSlugToDir = parseProductRootMapping(); + this.learnToFile = parseLearnToFileMapping(); + + // Track files that failed MDX validation + this.mdxValidationFailures = []; } async init() { @@ -106,7 +210,7 @@ class FernScribeGitHub { try { // Download the file content - const response = await fetch(file.url_private, { + const response = await httpRequest(file.url_private, { headers: { 'Authorization': `Bearer ${this.slackToken}` } @@ -151,7 +255,7 @@ class FernScribeGitHub { try { // Download image and convert to base64 - const response = await fetch(imageUrl, { + const response = await httpRequest(imageUrl, { headers: { 'Authorization': `Bearer ${this.slackToken}` } @@ -164,7 +268,7 @@ class FernScribeGitHub { const mimeType = response.headers.get('content-type') || 'image/jpeg'; // Use Claude to describe the image - const claudeResponse = await fetch('https://api.anthropic.com/v1/messages', { + const claudeResponse = await httpRequest('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'x-api-key': this.anthropicApiKey, @@ -215,7 +319,7 @@ class FernScribeGitHub { try { // Fetch the thread replies - const response = await fetch(`https://slack.com/api/conversations.replies?${new URLSearchParams({ + const response = await httpRequest(`https://slack.com/api/conversations.replies?${new URLSearchParams({ channel: parsedUrl.channelId, ts: parsedUrl.threadTs, inclusive: 'true' @@ -315,25 +419,117 @@ class FernScribeGitHub { } } - async createEmbedding(text) { - const response = await fetch('https://api.openai.com/v1/embeddings', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - input: text, - model: 'text-embedding-3-large', - }), - }); + // Estimate tokens (rough approximation: ~4 chars per token for English) + estimateTokens(text) { + return Math.ceil(text.length / 4); + } - if (!response.ok) { - throw new Error(`OpenAI API error: ${response.status}`); + // Truncate query intelligently to fit within token limits + truncateQuery(text, maxTokens = 8000) { // Leave some buffer below 8192 + const estimatedTokens = this.estimateTokens(text); + + if (estimatedTokens <= maxTokens) { + return text; } - const data = await response.json(); - return data.data[0].embedding; + console.log(`⚠️ Query too long (${estimatedTokens} tokens), truncating to fit within ${maxTokens} tokens...`); + + // Try to parse the enhanced query structure + const sections = text.split('\n\n'); + let truncatedText = ''; + let currentTokens = 0; + + // Prioritize sections: core request first, Slack discussion last + const prioritizedSections = []; + + for (const section of sections) { + if (section.includes('Add a comprehensive list') || section.startsWith('Issue:') || section.startsWith('Request:')) { + prioritizedSections.unshift(section); // High priority - add to beginning + } else if (section.includes('AI-suggested terms:')) { + prioritizedSections.splice(1, 0, section); // Medium-high priority + } else if (section.includes('Additional Context:')) { + prioritizedSections.splice(-1, 0, section); // Medium priority + } else if (section.includes('Slack Discussion Context:')) { + prioritizedSections.push(section); // Low priority - add to end + } else { + prioritizedSections.push(section); // Default priority + } + } + + // Build truncated query by adding sections until we hit the limit + for (const section of prioritizedSections) { + const sectionTokens = this.estimateTokens(section); + + if (currentTokens + sectionTokens <= maxTokens) { + if (truncatedText) truncatedText += '\n\n'; + truncatedText += section; + currentTokens += sectionTokens; + } else { + // If this is a Slack discussion, try to include a truncated version + if (section.includes('Slack Discussion Context:')) { + const remainingTokens = maxTokens - currentTokens; + const remainingChars = remainingTokens * 4; + + if (remainingChars > 200) { // Only add if we have meaningful space + const truncatedSection = section.slice(0, remainingChars - 50) + '\n\n[... Slack discussion truncated for token limit ...]'; + if (truncatedText) truncatedText += '\n\n'; + truncatedText += truncatedSection; + } + } + break; + } + } + + const finalTokens = this.estimateTokens(truncatedText); + console.log(`✂️ Truncated query: ${text.length} → ${truncatedText.length} chars (${estimatedTokens} → ${finalTokens} tokens)`); + + return truncatedText; + } + + async createEmbedding(text) { + console.log(`🔍 Creating embedding for text (${text.length} chars)...`); + + try { + // Truncate if necessary to fit within token limits + const truncatedText = this.truncateQuery(text); + + const response = await httpRequest('https://api.openai.com/v1/embeddings', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + input: truncatedText, + model: 'text-embedding-3-large', + }), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('❌ OpenAI API error details:', errorText); + + // Try to parse the error as JSON for better understanding + try { + const errorData = JSON.parse(errorText); + console.error('📋 Parsed error data:', JSON.stringify(errorData, null, 2)); + } catch (e) { + console.error('📋 Raw error text:', errorText); + } + + throw new Error(`OpenAI API error: ${response.status} - ${errorText}`); + } + + const data = await response.json(); + console.log(`✅ Embedding created successfully (${data.data[0]?.embedding?.length} dimensions)`); + return data.data[0].embedding; + + } catch (error) { + console.error('❌ createEmbedding failed:', error.message); + console.error('📝 Query preview (first 200 chars):', text.slice(0, 200)); + console.error('🔑 API Key present:', process.env.OPENAI_API_KEY ? `Yes (${process.env.OPENAI_API_KEY.length} chars)` : 'No'); + throw error; + } } reciprocalRankFusion(semanticResults, bm25Results, k = 60) { @@ -449,6 +645,17 @@ class FernScribeGitHub { } async generateContent(filePath, existingContent, context, fernStructure) { + // Check if content needs chunking + const CHUNK_THRESHOLD = 12000; // Chars threshold to decide when to chunk + if (existingContent.length <= CHUNK_THRESHOLD) { + return this.generateSingleContent(filePath, existingContent, context, fernStructure); + } else { + console.log(` 📊 Large file detected (${existingContent.length} chars) - using chunked processing`); + return this.generateChunkedContent(filePath, existingContent, context, fernStructure); + } + } + + async generateSingleContent(filePath, existingContent, context, fernStructure) { const prompt = `${this.systemPrompt} ## Context @@ -468,12 +675,20 @@ ${existingContent} ## Instructions Update this file to address the documentation request. Use the Slack discussion context to understand the specific pain points and requirements mentioned by users. Follow Fern documentation best practices and maintain consistency with the existing structure. +CRITICAL MDX SYNTAX REQUIREMENTS: +- ALL opening tags MUST have corresponding closing tags (e.g., must have ) +- Self-closing tags must use proper syntax (e.g., ) +- Preserve existing MDX component structure exactly +- When adding new ParamField, CodeBlock, or other components, ensure they are properly closed +- Check that every < has a matching > +- Validate that nested components are properly structured + IMPORTANT: Return ONLY the clean file content. Do not include any explanatory text, meta-commentary, or descriptions about what you're doing. Start directly with the frontmatter (---) or first line of the file content. Complete updated file content:`; try { - const response = await fetch('https://api.anthropic.com/v1/messages', { + const response = await httpRequest('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'x-api-key': this.anthropicApiKey, @@ -491,17 +706,317 @@ Complete updated file content:`; }); if (!response.ok) { - throw new Error(`Anthropic API error: ${response.status}`); + const errorText = await response.text(); + console.error('❌ Anthropic API error details (generateContent):', errorText); + throw new Error(`Anthropic API error: ${response.status} - ${errorText}`); } const data = await response.json(); - return data.content[0]?.text || ''; + const generatedContent = data.content[0]?.text || ''; + + // Basic MDX validation + const validationResult = this.validateMDXContent(generatedContent); + if (!validationResult.isValid) { + console.warn(`⚠️ MDX validation warnings for ${filePath}:`, validationResult.warnings); + } + + return generatedContent; } catch (error) { console.error('Claude API error:', error); return existingContent; // Return original if AI fails } } + async generateChunkedContent(filePath, existingContent, context, fernStructure) { + const chunks = this.chunkContent(existingContent, 8000); + const updatedChunks = []; + let hasChanges = false; + + console.log(` 🧩 Processing ${chunks.length} chunks for ${filePath}`); + + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + console.log(` 📝 Processing chunk ${i + 1}/${chunks.length}${chunk.section ? ` (${chunk.section})` : ''}`); + + const chunkPrompt = `${this.systemPrompt} + +## Context +File: ${filePath} +Chunk: ${i + 1} of ${chunks.length}${chunk.section ? ` - Section: "${chunk.section}"` : ''} +Request: ${context.requestDescription} +Existing Instructions: ${context.existingInstructions} +Why Current Approach Doesn't Work: ${context.whyNotWork} +Additional Context: ${context.additionalContext} +${context.slackThreadContent ? `\n## Slack Discussion Context\n${context.slackThreadContent}` : ''} + +## Fern Docs Structure Reference +${fernStructure} + +## Current Chunk Content +${chunk.content} + +## Instructions +${chunk.isComplete ? + 'This is the final chunk of the file. Update this section to address the documentation request.' : + `This is chunk ${i + 1} of ${chunks.length} from a larger file. Update only this section as needed to address the documentation request. Do not add or remove section headers unless specifically needed for this chunk.` +} + +Focus on: +- Addressing the specific documentation gaps mentioned in the request +- Improving clarity and completeness within this chunk +- Maintaining consistency with Fern documentation patterns +- Preserving the existing structure and flow + +CRITICAL MDX SYNTAX REQUIREMENTS: +- ALL opening tags MUST have corresponding closing tags (e.g., must have ) +- Self-closing tags must use proper syntax (e.g., ) +- Preserve existing MDX component structure exactly +- When adding new ParamField, CodeBlock, or other components, ensure they are properly closed +- Check that every < has a matching > +- Validate that nested components are properly structured + +IMPORTANT: Return ONLY the updated chunk content. Do not include any explanatory text, meta-commentary, or descriptions about what you're doing. + +Updated chunk content:`; + + try { + const response = await httpRequest('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': this.anthropicApiKey, + 'content-type': 'application/json', + 'anthropic-version': '2023-06-01' + }, + body: JSON.stringify({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 4096, + messages: [{ + role: 'user', + content: chunkPrompt + }] + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`❌ Anthropic API error for chunk ${i + 1}:`, errorText); + updatedChunks.push(chunk.content); // Use original chunk + continue; + } + + const data = await response.json(); + const updatedChunkContent = data.content[0]?.text || chunk.content; + + // Validate the chunk + const validationResult = this.validateMDXContent(updatedChunkContent); + if (!validationResult.isValid) { + console.warn(`⚠️ MDX validation warnings for chunk ${i + 1}:`, validationResult.warnings); + updatedChunks.push(chunk.content); // Use original chunk if validation fails + } else { + updatedChunks.push(updatedChunkContent); + if (updatedChunkContent !== chunk.content) { + hasChanges = true; + console.log(` ✅ Updated chunk ${i + 1} (${chunk.content.length} → ${updatedChunkContent.length} chars)`); + } else { + console.log(` ℹ️ No changes for chunk ${i + 1}`); + } + } + + } catch (error) { + console.error(`❌ Error processing chunk ${i + 1}:`, error.message); + updatedChunks.push(chunk.content); // Use original chunk + } + + // Add a small delay between chunks to be respectful to the API + if (i < chunks.length - 1) { + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + + // Reassemble the chunks + const finalContent = this.reassembleChunks(updatedChunks, chunks); + + console.log(` 🔧 Reassembled content: ${existingContent.length} → ${finalContent.length} chars`); + + return hasChanges ? finalContent : existingContent; + } + + reassembleChunks(updatedChunks, originalChunks) { + // If there's only one chunk, return it directly + if (updatedChunks.length === 1) { + return updatedChunks[0]; + } + + // For multiple chunks, we need to carefully reassemble + let reassembled = ''; + + for (let i = 0; i < updatedChunks.length; i++) { + const chunk = updatedChunks[i]; + const originalChunk = originalChunks[i]; + + if (i === 0) { + // First chunk should include frontmatter if present + reassembled = chunk; + } else { + // For subsequent chunks, remove frontmatter if it was duplicated + let cleanChunk = chunk; + if (cleanChunk.startsWith('---\n') && reassembled.includes('---\n')) { + // Remove frontmatter from subsequent chunks + const frontmatterEnd = cleanChunk.indexOf('---\n', 4); + if (frontmatterEnd !== -1) { + cleanChunk = cleanChunk.substring(frontmatterEnd + 4); + } + } + + // Add proper spacing between chunks + if (reassembled.trim() && cleanChunk.trim()) { + reassembled += '\n\n' + cleanChunk; + } else { + reassembled += cleanChunk; + } + } + } + + return reassembled; + } + + // Basic MDX validation to catch common issues + validateMDXContent(content) { + const warnings = []; + + // Check for unclosed ParamField tags + const paramFieldMatches = content.match(/]*>/g) || []; + const paramFieldCloses = content.match(/<\/ParamField>/g) || []; + if (paramFieldMatches.length !== paramFieldCloses.length) { + warnings.push(`Mismatched ParamField tags: ${paramFieldMatches.length} opening, ${paramFieldCloses.length} closing`); + } + + // Check for unclosed CodeBlock tags + const codeBlockMatches = content.match(/]*>/g) || []; + const codeBlockCloses = content.match(/<\/CodeBlock>/g) || []; + if (codeBlockMatches.length !== codeBlockCloses.length) { + warnings.push(`Mismatched CodeBlock tags: ${codeBlockMatches.length} opening, ${codeBlockCloses.length} closing`); + } + + // Check for other common unclosed tags + const commonTags = ['Accordion', 'AccordionItem', 'Tab', 'Tabs', 'Card']; + for (const tag of commonTags) { + const openTags = content.match(new RegExp(`<${tag}[^>]*>`, 'g')) || []; + const closeTags = content.match(new RegExp(`<\/${tag}>`, 'g')) || []; + if (openTags.length !== closeTags.length) { + warnings.push(`Mismatched ${tag} tags: ${openTags.length} opening, ${closeTags.length} closing`); + } + } + + return { + isValid: warnings.length === 0, + warnings + }; + } + + // Intelligent content chunking for large files + chunkContent(content, maxChunkSize = 8000) { + // If content is small enough, return as single chunk + if (content.length <= maxChunkSize) { + return [{ content, isComplete: true, chunkIndex: 0, totalChunks: 1 }]; + } + + const chunks = []; + const lines = content.split('\n'); + let currentChunk = ''; + let frontmatter = ''; + let inFrontmatter = false; + let frontmatterEnded = false; + + // Extract frontmatter first + if (lines[0] === '---') { + inFrontmatter = true; + for (let i = 0; i < lines.length; i++) { + if (i > 0 && lines[i] === '---') { + inFrontmatter = false; + frontmatterEnded = true; + frontmatter = lines.slice(0, i + 1).join('\n') + '\n'; + break; + } + } + } + + // Start processing from after frontmatter + const startIndex = frontmatterEnded ? lines.findIndex((line, idx) => idx > 0 && line === '---') + 1 : 0; + const contentLines = lines.slice(startIndex); + + let sectionBuffer = []; + let currentSection = null; + + for (let i = 0; i < contentLines.length; i++) { + const line = contentLines[i]; + + // Detect section headers (## or ###) + if (line.match(/^#{2,3}\s+/)) { + // If we have accumulated content and adding this section would exceed limit + if (sectionBuffer.length > 0 && (currentChunk + sectionBuffer.join('\n')).length > maxChunkSize) { + // Save current chunk + chunks.push({ + content: (chunks.length === 0 ? frontmatter : '') + currentChunk.trim(), + isComplete: false, + chunkIndex: chunks.length, + section: currentSection, + hasMore: true + }); + currentChunk = ''; + currentSection = null; + } + + // Start new section + currentSection = line.replace(/^#+\s+/, '').trim(); + sectionBuffer = [line]; + } else { + sectionBuffer.push(line); + } + + // Check if we need to break at this point + const potentialChunk = currentChunk + sectionBuffer.join('\n') + '\n'; + if (potentialChunk.length > maxChunkSize && currentChunk.length > 0) { + // Save current chunk without the current section + chunks.push({ + content: (chunks.length === 0 ? frontmatter : '') + currentChunk.trim(), + isComplete: false, + chunkIndex: chunks.length, + section: chunks.length > 0 ? currentSection : null, + hasMore: true + }); + currentChunk = sectionBuffer.join('\n') + '\n'; + sectionBuffer = []; + } else { + currentChunk += sectionBuffer.join('\n') + '\n'; + sectionBuffer = []; + } + } + + // Add remaining content as final chunk + if (currentChunk.trim()) { + chunks.push({ + content: (chunks.length === 0 ? frontmatter : '') + currentChunk.trim(), + isComplete: true, + chunkIndex: chunks.length, + section: currentSection, + hasMore: false + }); + } + + // Update totalChunks for all chunks + chunks.forEach(chunk => { + chunk.totalChunks = chunks.length; + }); + + console.log(` 📊 Split content into ${chunks.length} chunks (${content.length} chars total)`); + chunks.forEach((chunk, i) => { + console.log(` Chunk ${i + 1}: ${chunk.content.length} chars${chunk.section ? ` (${chunk.section})` : ''}`); + }); + + return chunks; + } + async analyzeDocumentationNeeds(context) { if (!this.anthropicApiKey) { console.log('⚠️ No Anthropic API key provided - skipping documentation analysis'); @@ -544,7 +1059,7 @@ Output your response as JSON: }`; try { - const response = await fetch('https://api.anthropic.com/v1/messages', { + const response = await httpRequest('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'x-api-key': this.anthropicApiKey, @@ -562,7 +1077,9 @@ Output your response as JSON: }); if (!response.ok) { - throw new Error(`Anthropic API error: ${response.status}`); + const errorText = await response.text(); + console.error('❌ Anthropic API error details (analyzeDocumentationNeeds):', errorText); + throw new Error(`Anthropic API error: ${response.status} - ${errorText}`); } const data = await response.json(); @@ -654,7 +1171,7 @@ Example format: Changelog entry:`; try { - const response = await fetch('https://api.anthropic.com/v1/messages', { + const response = await httpRequest('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'x-api-key': this.anthropicApiKey, @@ -674,7 +1191,9 @@ Changelog entry:`; }); if (!response.ok) { - throw new Error(`Anthropic API error: ${response.status}`); + const errorText = await response.text(); + console.error('❌ Anthropic API error details (generateChangelogEntry):', errorText); + throw new Error(`Anthropic API error: ${response.status} - ${errorText}`); } const data = await response.json(); @@ -690,6 +1209,186 @@ Changelog entry:`; return await this.urlMapper.mapTurbopufferPathToGitHub(turbopufferPath); } + // Returns the canonical product file path for a new file, using the mapping from my-mappings.md + getCanonicalProductFilePath(slugOrUrl, relPath) { + // Try to construct the /learn/... URL + let slug = null; + if (slugOrUrl) { + const match = /learn\/([\w-]+)/.exec(slugOrUrl); + if (match) slug = match[1]; + } + if (!slug) slug = slugOrUrl; + // Build the canonical /learn/... URL + let learnUrl = `/learn/${slug}/${relPath.replace(/\.mdx$/, '').replace(/\/+/, '/')}`; + // Remove any double slashes + learnUrl = learnUrl.replace(/\/+/g, '/'); + // Remove trailing .mdx if present + learnUrl = learnUrl.replace(/\.mdx$/, ''); + // Look up the mapping + const mappedPath = this.learnToFile[learnUrl]; + if (mappedPath) { + console.log(`[DEBUG] Using mapping: ${learnUrl} → ${mappedPath}`); + return mappedPath; + } else { + console.warn(`[DEBUG] No mapping found for ${learnUrl}, skipping file creation.`); + return null; + } + } + + // Find the appropriate product YAML file based on the file path + getProductYamlPath(filePath) { + if (filePath.includes('openapi-def') || filePath.includes('openapi-definition')) { + return 'fern/products/openapi-def/openapi-def.yml'; + } else if (filePath.includes('fern-def') || filePath.includes('fern-definition')) { + return 'fern/products/fern-def/fern-def.yml'; + } else if (filePath.includes('sdks')) { + return 'fern/products/sdks/sdks.yml'; + } else if (filePath.includes('docs')) { + return 'fern/products/docs/docs.yml'; + } else if (filePath.includes('ask-fern')) { + return 'fern/products/ask-fern/ask-fern.yml'; + } else if (filePath.includes('cli-api-reference')) { + return 'fern/products/cli-api-reference/cli-api-reference.yml'; + } else if (filePath.includes('asyncapi-def')) { + return 'fern/products/asyncapi-def/asyncapi-def.yml'; + } else if (filePath.includes('openrpc-def')) { + return 'fern/products/openrpc-def/openrpc-def.yml'; + } else if (filePath.includes('grpc-def')) { + return 'fern/products/grpc-def/grpc-def.yml'; + } + return null; + } + + // Extract the page information from a file path for YAML navigation + extractPageInfo(filePath, title) { + const pathParts = filePath.split('/'); + const fileName = pathParts[pathParts.length - 1].replace('.mdx', ''); + + // Create a slug from the file name + const slug = fileName; + + // Extract the relative path after 'fern/products/[product]/' + let relativePath = null; + const fernProductsIndex = pathParts.indexOf('products'); + if (fernProductsIndex >= 0 && fernProductsIndex + 2 < pathParts.length) { + // Get the path after 'fern/products/[product-name]/' + const pathAfterProduct = pathParts.slice(fernProductsIndex + 2).join('/'); + relativePath = './' + pathAfterProduct; + } + + // Try to find the appropriate section based on path + let section = null; + if (filePath.includes('extensions')) { + section = 'extensions'; + } else if (filePath.includes('configuration')) { + section = 'configuration'; + } else if (filePath.includes('generators')) { + section = 'generators'; + } else if (filePath.includes('overview')) { + section = 'overview'; + } + + return { + slug, + title: title || fileName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), + section, + path: relativePath + }; + } + + // Update product YAML file to include new page + async updateProductYaml(filePath, pageTitle, newPageCreated = false) { + if (!newPageCreated) { + return; // Only update YAML for new pages + } + + const yamlPath = this.getProductYamlPath(filePath); + if (!yamlPath) { + console.log(` ⚠️ Could not determine product YAML for ${filePath}`); + return; + } + + try { + console.log(` 📝 Updating product navigation: ${yamlPath}`); + + // Fetch current YAML content + const currentYaml = await this.fetchFileContent(yamlPath); + if (!currentYaml) { + console.log(` ⚠️ Could not fetch YAML file: ${yamlPath}`); + return; + } + + // Parse YAML + const yamlData = yaml.load(currentYaml); + const pageInfo = this.extractPageInfo(filePath, pageTitle); + + // Find the appropriate section to add the new page + let targetSection = null; + if (yamlData.navigation) { + // Look for existing section + for (const item of yamlData.navigation) { + if (item.section === pageInfo.section) { + targetSection = item; + break; + } + } + + // If no specific section found, add to the end + if (!targetSection && yamlData.navigation.length > 0) { + // Find a good parent section or create one + if (pageInfo.section === 'extensions') { + targetSection = yamlData.navigation.find(item => + item.section === 'extensions' || + item.title?.toLowerCase().includes('extension') + ); + } + + if (!targetSection) { + // Add to the last section that has children + targetSection = yamlData.navigation.find(item => item.contents); + } + } + } + + // Add the new page + const newPageEntry = { + page: pageInfo.slug, + title: pageInfo.title, + path: pageInfo.path + }; + + if (targetSection && targetSection.contents) { + targetSection.contents.push(newPageEntry); + } else if (yamlData.navigation) { + // Create a new section if needed + yamlData.navigation.push({ + section: pageInfo.section || 'other', + contents: [newPageEntry] + }); + } else { + // Fallback: create basic navigation structure + yamlData.navigation = [{ + section: pageInfo.section || 'main', + contents: [newPageEntry] + }]; + } + + // Convert back to YAML + const updatedYaml = yaml.dump(yamlData, { + indent: 2, + lineWidth: -1, + noRefs: true + }); + + console.log(` ✅ Added page "${pageInfo.title}" to ${yamlPath}`); + return { yamlPath, updatedYaml }; + + } catch (error) { + console.error(` ❌ Error updating YAML for ${filePath}:`, error.message); + return null; + } + } + // Simple file content fetcher for dynamic mapping (without path transformation) async fetchFileContent(filePath) { try { @@ -798,7 +1497,9 @@ Changelog entry:`; async createPullRequest(branchName, context, filesUpdated) { const title = `🌿 Fern Scribe: ${context.requestDescription.substring(0, 50)}...`; - const body = `## 🌿 Fern Scribe Documentation Update + + // Build the main PR body + let body = `## 🌿 Fern Scribe Documentation Update **Original Request:** ${context.requestDescription} @@ -809,9 +1510,37 @@ ${filesUpdated.map(file => `- \`${file}\``).join('\n')} ${context.slackThread ? `**Related Discussion:** ${context.slackThread}` : ''} -${context.additionalContext ? `**Additional Context:** ${context.additionalContext}` : ''} +${context.additionalContext ? `**Additional Context:** ${context.additionalContext}` : ''}`; + + // Add section for files that failed MDX validation + if (this.mdxValidationFailures.length > 0) { + body += `\n\n## ⚠️ Files with MDX Validation Issues + +The following files could not be updated due to MDX validation failures after 3 attempts: + +${this.mdxValidationFailures.map((failure, index) => { + const warnings = failure.warnings.map(w => ` - ${w}`).join('\n'); + const truncatedContent = failure.suggestedContent && failure.suggestedContent.length > 4000 + ? failure.suggestedContent.substring(0, 4000) + '\n\n... [Content truncated due to length]' + : failure.suggestedContent; + + return `### ${index + 1}. **\`${failure.filePath}\`** (${failure.title || 'Untitled'}) ---- +- **URL**: ${failure.url || 'N/A'} +- **Validation Issues**: +${warnings} + +**Suggested Content** (needs manual MDX fixes): + +\`\`\`mdx +${truncatedContent || 'No suggested content available'} +\`\`\``; +}).join('\n\n')} + +**Note**: These files require manual review and correction of their MDX component structure before the content can be applied.`; + } + + body += `\n\n--- *This PR was automatically generated by Fern Scribe based on issue #${this.issueNumber}* **Please review the changes carefully before merging.**`; @@ -953,7 +1682,48 @@ ${context.additionalContext ? `**Additional Context:** ${context.additionalConte }; console.log(` 🤖 Generating AI suggestions based on context...`); - const suggestedContent = await this.generateContent(filePath, currentContent, contextWithDocument, fernStructure); + let suggestedContent = null; + let valid = false; + let attempts = 0; + while (attempts < 3 && !valid) { + suggestedContent = await this.generateContent(filePath, currentContent, contextWithDocument, fernStructure); + const validationResult = this.validateMDXContent(suggestedContent); + if (validationResult.isValid) { + valid = true; + } else { + attempts++; + console.warn(`⚠️ MDX validation failed for ${filePath} (attempt ${attempts}):`, validationResult.warnings); + // Optionally: try to auto-fix here (not implemented yet) + } + } + if (!valid) { + const validationResult = this.validateMDXContent(suggestedContent); + const msg = `❌ Skipping file due to invalid MDX after 3 attempts: ${filePath}\nWarnings: ${JSON.stringify(validationResult.warnings)}`; + console.warn(msg); + + // Track this failure for the PR description + this.mdxValidationFailures.push({ + filePath, + warnings: validationResult.warnings, + attempts: 3, + url: result.url, + title: result.title, + suggestedContent: suggestedContent // Store the suggested content despite validation issues + }); + + // If running in GitHub Actions, comment on the issue + if (process.env.GITHUB_TOKEN && process.env.REPOSITORY && process.env.ISSUE_NUMBER) { + const [owner, repo] = process.env.REPOSITORY.split('/'); + const octokit = this.octokit; + await octokit.rest.issues.createComment({ + owner, + repo, + issue_number: this.issueNumber, + body: msg + }); + } + continue; // Skip this file + } if (suggestedContent && suggestedContent !== currentContent) { analysisResults.push({ @@ -1055,17 +1825,63 @@ ${context.additionalContext ? `**Additional Context:** ${context.additionalConte // Update files with suggested content for (const result of analysisResults) { try { - const actualPath = await this.mapTurbopufferPathToGitHub(result.filePath); + let actualPath; + const isNewFile = result.currentContent.length === 0; + if (isNewFile) { + // Use mapping to get correct product directory for new files + let slug = null; + if (result.url) { + const match = /learn\/([\w-]+)/.exec(result.url); + if (match) slug = match[1]; + } + if (!slug && result.filePath) { + const match = /learn\/([\w-]+)/.exec(result.filePath); + if (match) slug = match[1]; + } + // Extract relative path after /learn// + let relPath = ''; + if (result.url) { + const relMatch = result.url.match(/learn\/[\w-]+\/(.*)/); + if (relMatch) relPath = relMatch[1]; + } + if (!relPath && result.filePath) { + const relMatch = result.filePath.match(/learn\/[\w-]+\/(.*)/); + if (relMatch) relPath = relMatch[1]; + } + relPath = relPath.replace(/\.mdx$/, '') + '.mdx'; + actualPath = this.getCanonicalProductFilePath(slug, relPath); + if (!actualPath) { + console.warn(`[DEBUG] Skipping file creation for ${result.url || result.filePath} (no mapping found)`); + continue; + } + } else { + actualPath = await this.mapTurbopufferPathToGitHub(result.filePath); + } - console.log(` 📝 Updating file: ${actualPath}`); + console.log(` 📝 Updating file: ${actualPath}${isNewFile ? ' (new file)' : ''}`); await this.updateFile( actualPath, result.suggestedContent, branchName, - `Update ${path.basename(actualPath)} based on issue #${this.issueNumber}` + `${isNewFile ? 'Create' : 'Update'} ${path.basename(actualPath)} based on issue #${this.issueNumber}` ); filesUpdated.push(actualPath); + + // Update product YAML if this is a new file + if (isNewFile) { + const yamlUpdate = await this.updateProductYaml(actualPath, result.title, true); + if (yamlUpdate) { + console.log(` 📝 Updating navigation: ${yamlUpdate.yamlPath}`); + await this.updateFile( + yamlUpdate.yamlPath, + yamlUpdate.updatedYaml, + branchName, + `Add ${result.title} page to navigation` + ); + filesUpdated.push(yamlUpdate.yamlPath); + } + } } catch (error) { console.error(` ⚠️ Could not update ${result.filePath}: ${error.message}`); } diff --git a/.github/scripts/fern-url-mapper.js b/.github/scripts/fern-url-mapper.js index e3aa7e7d8..772ffae9c 100644 --- a/.github/scripts/fern-url-mapper.js +++ b/.github/scripts/fern-url-mapper.js @@ -5,7 +5,9 @@ const fs = require('fs').promises; class FernUrlMapper { constructor(githubToken = null, repository = null) { this.dynamicPathMapping = new Map(); + this.staticPathMapping = new Map(); this.isPathMappingLoaded = false; + this.isStaticMappingLoaded = false; // Initialize GitHub client if credentials provided if (githubToken && repository) { @@ -40,6 +42,35 @@ class FernUrlMapper { } } + // Load static path mapping from my-mappings.md + async loadStaticPathMapping() { + if (this.isStaticMappingLoaded) return; + + try { + const mappingsContent = await fs.readFile('my-mappings.md', 'utf-8'); + console.log('Loading static path mappings from my-mappings.md...'); + + // Parse the markdown file for URL mappings + const lines = mappingsContent.split('\n'); + let mappingCount = 0; + + for (const line of lines) { + // Look for lines that match the mapping pattern: - `/learn/...` → `fern/...` + const match = line.match(/^-\s+`([^`]+)`\s+→\s+`([^`]+)`/); + if (match) { + const [, url, path] = match; + this.staticPathMapping.set(url, path); + mappingCount++; + } + } + + this.isStaticMappingLoaded = true; + console.log(`Loaded ${mappingCount} static path mappings from my-mappings.md`); + } catch (error) { + console.error('Failed to load static path mapping:', error.message); + } + } + // Load dynamic path mapping from Fern docs structure async loadDynamicPathMapping() { if (this.isPathMappingLoaded) return; @@ -222,10 +253,15 @@ class FernUrlMapper { // Transform Turbopuffer URLs to actual GitHub file paths transformTurbopufferUrlToPath(turbopufferUrl) { - // Clean up trailing slashes but keep the /learn prefix for dynamic mapping lookup + // Clean up trailing slashes but keep the /learn prefix for mapping lookup let cleanUrl = turbopufferUrl.replace(/\/$/, ''); - // First try to use dynamic mapping with full URL (including /learn) + // First try to use static mapping from my-mappings.md + if (this.staticPathMapping.has(cleanUrl)) { + return this.staticPathMapping.get(cleanUrl); + } + + // Second try to use dynamic mapping with full URL (including /learn) if (this.dynamicPathMapping.has(cleanUrl)) { const mappedPath = this.dynamicPathMapping.get(cleanUrl); // Add .mdx extension if not present and not already a complete path @@ -279,22 +315,33 @@ class FernUrlMapper { } } - // Map Turbopuffer URLs to actual GitHub file paths (now using dynamic mapping) + // Map Turbopuffer URLs to actual GitHub file paths (now using static mapping first, then dynamic) async mapTurbopufferPathToGitHub(turbopufferPath) { - // Ensure dynamic mapping is loaded + // Ensure static mapping is loaded first + await this.loadStaticPathMapping(); + // Ensure dynamic mapping is loaded as fallback await this.loadDynamicPathMapping(); - // Use the improved transformation logic that prioritizes dynamic mapping + // Use the improved transformation logic that prioritizes static mapping, then dynamic mapping return this.transformTurbopufferUrlToPath(turbopufferPath) || turbopufferPath; } // Get all mappings as an object for external use async getAllMappings() { + await this.loadStaticPathMapping(); await this.loadDynamicPathMapping(); const mappings = {}; + + // Add dynamic mappings first for (const [url, path] of this.dynamicPathMapping) { mappings[url] = path; } + + // Override with static mappings (they take priority) + for (const [url, path] of this.staticPathMapping) { + mappings[url] = path; + } + return mappings; } @@ -341,6 +388,7 @@ class FernUrlMapper { // Test specific URL mappings async testMappings(testUrls = []) { + await this.loadStaticPathMapping(); await this.loadDynamicPathMapping(); console.log('\n=== TESTING URL MAPPINGS ==='); diff --git a/.github/scripts/generate-mappings.js b/.github/scripts/generate-mappings.js new file mode 100644 index 000000000..6fa6c1405 --- /dev/null +++ b/.github/scripts/generate-mappings.js @@ -0,0 +1,229 @@ +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); +const { Octokit } = require('@octokit/rest'); + +const OUTPUT_FILE = path.join(__dirname, 'my-mappings.md'); +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const REPOSITORY = process.env.REPOSITORY; +const BRANCH = 'main'; + +if (!GITHUB_TOKEN || !REPOSITORY) { + console.error('GITHUB_TOKEN and REPOSITORY env vars are required.'); + process.exit(1); +} + +const [owner, repo] = REPOSITORY.split('/'); +const octokit = new Octokit({ auth: GITHUB_TOKEN }); + +async function listDir(pathInRepo) { + try { + const res = await Promise.race([ + octokit.repos.getContent({ + owner, + repo, + path: pathInRepo, + ref: BRANCH + }), + new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 10000)) + ]); + return Array.isArray(res.data) ? res.data : []; + } catch (e) { + return []; + } +} + +async function getFileContent(pathInRepo) { + try { + const res = await Promise.race([ + octokit.repos.getContent({ + owner, + repo, + path: pathInRepo, + ref: BRANCH + }), + new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 10000)) + ]); + if (res.data && res.data.content) { + return Buffer.from(res.data.content, 'base64').toString('utf-8'); + } + return null; + } catch (e) { + return null; + } +} + +function slugify(str) { + return String(str) + .replace(/(-def|-reference|-docs|-api)$/i, '') // Only remove at the end + .replace(/_/g, '-') + .replace(/\s+/g, '-') + .toLowerCase(); +} + +async function findDocsYml(productDir) { + // Try .yml, docs.yml, or any .yml in the product dir + const files = await listDir(`fern/products/${productDir}`); + const candidates = [ + `${productDir}.yml`, + `docs.yml` + ]; + for (const candidate of candidates) { + if (files.find(f => f.name === candidate)) { + return candidate; + } + } + // fallback: first .yml file + const yml = files.find(f => f.name.endsWith('.yml')); + return yml ? yml.name : null; +} + +async function findPageFile(productDir, page) { + // Try to find the .mdx file in pages/ recursively using the API + async function walk(dir) { + console.log(`[DEBUG] Listing directory: ${dir}`); + const items = await listDir(dir); + for (const item of items) { + if (item.type === 'dir') { + const found = await walk(item.path); + if (found) return found; + } else if (item.name.replace(/\.mdx$/, '') === page) { + console.log(`[DEBUG] Found page file: ${item.path} for page: ${page}`); + return item.path; + } + } + return null; + } + return await walk(`fern/products/${productDir}/pages`); +} + +async function walkNav(nav, parentSlugs, pages, productDir, canonicalSlug, depth = 0) { + for (const item of nav) { + let sectionSlug = ''; + if (item['skip-slug']) { + sectionSlug = ''; + console.log(`[DEBUG] [${' '.repeat(depth)}] Skipping slug for section: ${item.section || ''}`); + } else if (item.slug === true && item.section) { + sectionSlug = slugify(item.section); + console.log(`[DEBUG] [${' '.repeat(depth)}] Section with slug:true: ${sectionSlug}`); + } else if (typeof item.slug === 'string') { + sectionSlug = slugify(item.slug); + console.log(`[DEBUG] [${' '.repeat(depth)}] Section with explicit slug: ${sectionSlug}`); + } else if (item.section) { + sectionSlug = slugify(item.section); + console.log(`[DEBUG] [${' '.repeat(depth)}] Section with name: ${sectionSlug}`); + } + const newSlugs = sectionSlug ? [...parentSlugs, sectionSlug] : parentSlugs; + if (item.contents) { + console.log(`[DEBUG] [${' '.repeat(depth)}] Entering section: ${sectionSlug || '(no slug)'} with path: /learn/${[canonicalSlug, ...newSlugs].join('/')}`); + await walkNav(item.contents, newSlugs, pages, productDir, canonicalSlug, depth + 1); + console.log(`[DEBUG] [${' '.repeat(depth)}] Exiting section: ${sectionSlug || '(no slug)'}`); + } + if (item.page) { + let pageSlug = typeof item.slug === 'string' ? slugify(item.slug) : slugify(item.page); + // Only add pageSlug if it's not the same as the last section slug + let urlSegments = ['/learn', canonicalSlug, ...newSlugs]; + if (newSlugs[newSlugs.length - 1] !== pageSlug) { + urlSegments.push(pageSlug); + } + const learnUrl = urlSegments.filter(Boolean).join('/'); + if (item.path) { + // Remove leading './' if present + let repoPath = item.path.replace(/^\.\//, ''); + // Always make it relative to fern/products/ + if (!repoPath.startsWith('fern/products/')) { + repoPath = `fern/products/${productDir}/${repoPath}`; + } + pages.push({ learnUrl, repoPath }); + console.log(`[DEBUG] [${' '.repeat(depth)}] Mapping: ${learnUrl} → ${repoPath}`); + } else { + console.warn(`[DEBUG] [${' '.repeat(depth)}] Skipping page: ${item.page} in product: ${productDir} (missing path)`); + } + } + } +} + +async function main() { + // Step 1: Find and parse the root docs.yml + const rootDocsYmlContent = await getFileContent('fern/docs.yml'); + if (!rootDocsYmlContent) { + console.error('Could not find fern/docs.yml in the repo.'); + process.exit(1); + } + const rootDocsYml = yaml.load(rootDocsYmlContent); + + // Step 2: Get the root URL subpath (e.g., /learn) + let rootUrlSubpath = ''; + if (rootDocsYml.url) { + const url = rootDocsYml.url; + const match = url.match(/https?:\/\/[^/]+(\/.*)/); + rootUrlSubpath = match ? match[1].replace(/\/$/, '') : ''; + console.log(`[DEBUG] Root URL subpath: ${rootUrlSubpath}`); + } + if (!rootUrlSubpath) rootUrlSubpath = '/learn'; + console.log(`[DEBUG] rootUrlSubpath: "${rootUrlSubpath}"`); + + // Step 3: Parse products from root docs.yml + const products = rootDocsYml.products || []; + let rootMappingLines = ['## Product Root Directories', '']; + let slugToDir = {}; + let allPages = []; + for (const product of products) { + if (!product.path || !product.slug) { + console.warn(`[DEBUG] Skipping product with missing path or slug: ${JSON.stringify(product)}`); + continue; + } + // product.path is like ./products/openapi-def/openapi-def.yml + const productDir = product.path.split('/')[2]; + const productYmlPath = product.path.replace('./', 'fern/'); + const ymlContent = await getFileContent(productYmlPath); + if (!ymlContent) { + console.warn(`[DEBUG] Could not fetch product YAML: ${productYmlPath}`); + continue; + } + const productYml = yaml.load(ymlContent); + const canonicalSlug = slugify(product.slug); + slugToDir[canonicalSlug] = productDir; + rootMappingLines.push(`${canonicalSlug}: ${productDir}`); + console.log(`[DEBUG] Product: ${productDir}, Slug: ${canonicalSlug}, YAML: ${productYmlPath}`); + if (productYml && productYml.navigation) { + await walkNav(productYml.navigation, [], allPages, productDir, canonicalSlug); + } else { + console.warn(`[DEBUG] No navigation found in ${productYmlPath}`); + } + } + rootMappingLines.push(''); + + let lines = [ + '# Fern URL Mappings', + '', + `Generated on: ${new Date().toISOString()}`, + '', + ...rootMappingLines, + '## Products', + '' + ]; + let total = 0; + for (const slug in slugToDir) { + lines.push(`## ${slug.charAt(0).toUpperCase() + slug.slice(1)}`); + lines.push(''); + const pages = allPages.filter(p => p.learnUrl.startsWith(`/learn/${slug}/`)); + if (pages.length === 0) { + lines.push('_No .mdx files found for this product._'); + } + for (const { learnUrl, repoPath } of pages) { + lines.push(`- \`${learnUrl}\` → \`${repoPath}\``); + console.log(`[DEBUG] Mapping: ${learnUrl} → ${repoPath}`); + total++; + } + lines.push(''); + } + lines[3] = `Total mappings: ${total}`; + fs.writeFileSync(OUTPUT_FILE, lines.join('\n'), 'utf-8'); + console.log(`Wrote ${total} mappings to ${OUTPUT_FILE}`); + if (total === 0) { + console.warn('Warning: No mappings were generated. Check your repo structure and permissions.'); + } +} + +main(); \ No newline at end of file diff --git a/.github/scripts/package.json b/.github/scripts/package.json index bf1c049e4..55ecc159f 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -6,7 +6,6 @@ "dependencies": { "@octokit/rest": "^20.0.2", "@turbopuffer/turbopuffer": "^0.10.14", - "node-fetch": "^3.3.2", "js-yaml": "^4.1.0" }, "engines": { diff --git a/.github/workflows/fern-scribe.yml b/.github/workflows/fern-scribe.yml index a41498cdc..99ebdb931 100644 --- a/.github/workflows/fern-scribe.yml +++ b/.github/workflows/fern-scribe.yml @@ -32,6 +32,13 @@ jobs: cd .github/scripts npm install + # --- NEW STEP: Generate my-mappings.md --- + - name: Generate Fern URL Mappings + run: | + cd .github/scripts + node generate-mappings.js + # ----------------------------------------- + - name: Run Fern Scribe env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/fern/docs.yml b/fern/docs.yml index a03470043..d0944cc93 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -42,13 +42,6 @@ products: image: ./images/product-switcher/product-switcher-askfern-light.png slug: ask-fern subtitle: Let users find answers in your documentation instantly - - - # - display-name: API Definition - # path: ./products/api-definition/api-definition.yml - # icon: fa-regular fa-book - # image: ./images/product-switcher/api-definitions-light.png - # slug: api-definition - display-name: OpenAPI path: ./products/openapi-def/openapi-def.yml diff --git a/fern/images/product-switcher/product-switcher-askfern-dark.png b/fern/images/product-switcher/product-switcher-askfern-dark.png index 9092dcea1..d0c29de53 100644 Binary files a/fern/images/product-switcher/product-switcher-askfern-dark.png and b/fern/images/product-switcher/product-switcher-askfern-dark.png differ diff --git a/fern/images/product-switcher/product-switcher-askfern-light.png b/fern/images/product-switcher/product-switcher-askfern-light.png index 3332c1351..200235328 100644 Binary files a/fern/images/product-switcher/product-switcher-askfern-light.png and b/fern/images/product-switcher/product-switcher-askfern-light.png differ diff --git a/fern/products/api-definition/api-definition.yml b/fern/products/api-definition/api-definition.yml deleted file mode 100644 index ec4fbbe66..000000000 --- a/fern/products/api-definition/api-definition.yml +++ /dev/null @@ -1,153 +0,0 @@ -navigation: - - section: Introduction - contents: - - page: What is an API Definition? - icon: fa-regular fa-question-circle - path: ./pages/introduction/what-is-an-api-definition.mdx - - page: What is the Fern Folder? - icon: fa-regular fa-folder - path: ./pages/introduction/what-is-the-fern-folder.mdx - - section: OpenAPI Specification - slug: openapi - contents: - - page: Overview - icon: fa-regular fa-brackets-curly - path: ./pages/openapi/overview.mdx - - page: Authentication - icon: fa-regular fa-lock-keyhole - path: ./pages/openapi/auth.mdx - - page: Servers - icon: fa-regular fa-globe - path: ./pages/openapi/servers.mdx - - section: Endpoints - icon: fa-regular fa-object-intersect - slug: endpoints - contents: - - page: HTTP JSON Endpoints - icon: fa-regular fa-display-code - path: ./pages/openapi/endpoints/rest.mdx - slug: http - - page: Multipart Form Uploads - icon: fa-regular fa-file - path: ./pages/openapi/endpoints/multipart.mdx - slug: multipart - - page: Server-Sent Events - path: ./pages/openapi/endpoints/sse.mdx - icon: fa-regular fa-signal-stream - slug: sse - - page: Webhooks - path: ./pages/openapi/webhooks.mdx - icon: fa-regular fa-webhook - - page: Audiences - icon: fa-duotone fa-users - path: ./pages/openapi/extensions/audiences.mdx - slug: audiences - - section: Extensions - icon: fa-regular fa-object-intersect - slug: extensions - contents: - - page: SDK Method Names - icon: fa-regular fa-display-code - path: ./pages/openapi/extensions/method-names.mdx - slug: method-names - - page: Parameter Names - icon: fa-regular fa-input-text - path: ./pages/openapi/extensions/parameter-names.mdx - - page: Other - icon: fa-regular fa-ellipsis-h - path: ./pages/openapi/extensions/others.mdx - slug: others - - page: Overlay Customizations - icon: fa-regular fa-shuffle - path: ./pages/openapi/overrides.mdx - - page: Sync your OpenAPI Specification - icon: fa-regular fa-arrows-rotate - path: ./pages/openapi/automation.mdx - - section: Integrate your Server Framework - icon: fa-regular fa-server - slug: frameworks - contents: - - page: FastAPI - icon: fa-regular fa-circle-bolt - path: ./pages/openapi/server-frameworks/fastapi.mdx - slug: fastapi - - section: Fern Definition - slug: fern - contents: - - page: Overview - icon: fa-regular fa-seedling - path: ./pages/fern-definition/overview.mdx - - page: Authentication - icon: fa-regular fa-lock-keyhole - path: ./pages/fern-definition/auth.mdx - - page: Types - icon: fa-regular fa-shapes - path: ./pages/fern-definition/types.mdx - - section: Endpoints - icon: fa-regular fa-plug - path: ./pages/fern-definition/endpoints.mdx - contents: - - page: HTTP JSON Endpoints - icon: fa-regular fa-display-code - path: ./pages/fern-definition/endpoints/rest.mdx - slug: http - - page: Multipart Form Uploads - icon: fa-regular fa-file - path: ./pages/fern-definition/endpoints/multipart.mdx - slug: multipart - - page: Bytes - path: ./pages/fern-definition/endpoints/bytes.mdx - icon: fa-regular fa-server - slug: bytes - - page: Server-Sent Events - icon: fa-regular fa-signal-stream - path: ./pages/fern-definition/endpoints/sse.mdx - slug: sse - - page: Webhooks - icon: fa-regular fa-webhook - path: ./pages/fern-definition/webhooks.mdx - - page: WebSockets - icon: fa-regular fa-globe - path: ./pages/fern-definition/websockets.mdx - slug: websockets - - page: Errors - icon: fa-regular fa-exclamation-triangle - path: ./pages/fern-definition/errors.mdx - - page: Imports - icon: fa-regular fa-download - path: ./pages/fern-definition/imports.mdx - - page: Examples - icon: fa-regular fa-square-terminal - path: ./pages/fern-definition/examples.mdx - - page: Audiences - icon: fa-duotone fa-users - path: ./pages/fern-definition/audiences.mdx - - page: Availability - icon: fa-regular fa-clock-rotate-left - path: ./pages/fern-definition/availability.mdx - - section: api.yml Reference - icon: fa-regular fa-books - slug: api-yml - contents: - - page: Overview - icon: fa-regular fa-book - path: ./pages/fern-definition/api-yml/overview.mdx - - page: Environments - icon: fa-regular fa-circle-wifi - path: ./pages/fern-definition/api-yml/environments.mdx - - page: Global Headers - icon: fa-regular fa-globe - path: ./pages/fern-definition/api-yml/global-configuration.mdx - - page: Errors - icon: fa-regular fa-exclamation-triangle - path: ./pages/fern-definition/api-yml/errors.mdx - - page: Packages - icon: fa-regular fa-box-open - path: ./pages/fern-definition/packages.mdx - - page: Depending on Other APIs - icon: fa-regular fa-link - path: ./pages/fern-definition/depending-on-other-apis.mdx - - page: Export to OpenAPI - icon: fa-regular fa-file-export - slug: export-openapi - path: ./pages/fern-definition/export-openapi.mdx diff --git a/fern/products/api-definition/pages/fern-definition/api-yml/environments.mdx b/fern/products/api-definition/pages/fern-definition/api-yml/environments.mdx deleted file mode 100644 index bdb9cc691..000000000 --- a/fern/products/api-definition/pages/fern-definition/api-yml/environments.mdx +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: Environments -description: List environments like production, staging, and development. ---- - -You can specify the environments where your server is deployed. - -## Single URL environments - -```yaml title="api.yml" -name: api -environments: - Production: https://www.yoursite.com - Staging: - docs: This staging environment is helpful for testing! - url: https://www.staging.yoursite.com -``` - -## Multiple URLs per environment - -You can specify multiple URLs per environment. This is helpful if you have a -microservice architecture, and you want a single SDK to interact with multiple -servers. - -```yaml title="api.yml" -environments: - Production: - urls: - Auth: https://auth.yoursite.com - Plants: https://plants.yoursite.com - Staging: - urls: - Auth: https://auth.staging.yoursite.com - Plants: https://plants.staging.yoursite.com -``` - -If you choose to use this feature, you must specify a `url` for each service you define: - -```yaml title="auth.yml" -service: - url: Auth - base-path: /auth - ... -``` - -## Default environment - -You can also provide a default environment: - -```yaml title="api.yml" -name: api -environments: - Production: https://www.yoursite.com - Staging: - docs: This staging environment is helpful for testing! - url: https://www.staging.yoursite.com -default-environment: Production -``` - - By providing a default environment, the generated SDK will be setup to hit that URL out-of-the-box. - -## Base path -If you would like all of your endpoints to be prefixed with a path, use `base-path`. - -In the example below, every endpoint is prefixed with a `/v1`: -```yaml title="api.yml" -name: api -base-path: /v1 -``` - -## Audiences - -If you have listed environments that you want to filter, you can leverage audiences. - -```yaml title="api.yml" -audiences: - - public - -environments: - Dev: - url: https://api.dev.buildwithfern.com - Prod: - url: https://api.buildwithfern.com - audiences: - - external -``` - diff --git a/fern/products/api-definition/pages/fern-definition/api-yml/errors.mdx b/fern/products/api-definition/pages/fern-definition/api-yml/errors.mdx deleted file mode 100644 index 06ce9ef3f..000000000 --- a/fern/products/api-definition/pages/fern-definition/api-yml/errors.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Errors -description: Specify error types and schemas ---- - -In order to generate SDKs idiomatically, Fern needs to know how to differentiate -between different errors when parsing an endpoint response. - -### Discriminate by status code - -You can specify Fern to discriminate by status code. This means on each -endpoint, every error that's listed must have a different HTTP status code. - - -```yaml -name: api -error-discrimination: - strategy: status-code -``` - - -### Discriminate by error name - -You can specify Fern to discriminate by error name. If you select this strategy, -then Fern will assume that every error response has an extra property denoting -the error name. - -If you use Fern to generate server-side code, then this option provides -the most flexibility. Otherwise, you'll probably want to use the status code -discrimination strategy. - - -```yaml -name: api -error-discrimination: - strategy: property - property-name: errorName -``` - - -### Global errors - -You can import and list errors that will be thrown by every endpoint. - - -```yaml -imports: - commons: commons.yml - -errors: - - commons.NotFoundError - - commons.BadRequestError -``` - \ No newline at end of file diff --git a/fern/products/api-definition/pages/fern-definition/api-yml/global-configuration.mdx b/fern/products/api-definition/pages/fern-definition/api-yml/global-configuration.mdx deleted file mode 100644 index 0f3cc3b7d..000000000 --- a/fern/products/api-definition/pages/fern-definition/api-yml/global-configuration.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -title: Global Configuration -description: Specify global headers, path parameters or query parameters meant to be included on every request. ---- - -The `api.yml` configuration supports global configuration like headers and path parameters. - -## Global headers - -You can specify headers that are meant to be included on every request: - - -```yaml -name: api -headers: - X-App-Id: string -``` - - -## Global path parameters - -You can specify path parameters that are meant to be included on every request: - - -```yaml -name: api -base-path: /{userId}/{orgId} -path-parameters: - userId: string - orgId: string -``` - - -### Overriding the base path - -If you have certain endpoints that do not live at the configured `base-path`, you can -override the `base-path` at the endpoint level. - -```yml imdb.yml {5} -service: - endpoints: - getMovie: - method: POST - base-path: "override/{arg}" - path: "movies/{movieId}" - path-parameters: - arg: string -``` - -## Global query parameters - -You cannot yet specify query parameters that are meant to be included on every request. -If you'd like to see this feature, please upvote [this issue](https://github.com/fern-api/fern/issues/2930). \ No newline at end of file diff --git a/fern/products/api-definition/pages/fern-definition/api-yml/overview.mdx b/fern/products/api-definition/pages/fern-definition/api-yml/overview.mdx deleted file mode 100644 index d27912ad7..000000000 --- a/fern/products/api-definition/pages/fern-definition/api-yml/overview.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -title: The api.yml configuration file -description: The api.yml file contains general API configuration when using the Fern Definition format. ---- - -A `fern/` folder has a special file called `api.yml`, which includes all the API-wide configuration. - -```bash {5} -fern/ -├─ fern.config.json -├─ generators.yml -└─ definition/ - ├─ api.yml - ├─ pet.yml - ├─ store.yml - └─ user.yml -``` - -## API name - -This name is used to uniquely identify your API in your organization. If you just have one API, then `api` is a sufficient name. - - -```yaml -name: api -``` - - -## API description - -You can define a top level API description. This description will come through in the OpenAPI Specification and Postman collection. - - -```yaml {2-4} -name: api -docs: | - ## Header - This API provides access to... -``` - - -## API version - -You can define your header-based API versioning scheme, such as an `X-API-Version`. The supported versions -and default value are specified like so: - - -```yaml -version: - header: X-API-Version - default: "2.0.0" - values: - - "1.0.0" - - "2.0.0" - - "latest" -``` - \ No newline at end of file diff --git a/fern/products/api-definition/pages/fern-definition/audiences.mdx b/fern/products/api-definition/pages/fern-definition/audiences.mdx deleted file mode 100644 index 20f2876a3..000000000 --- a/fern/products/api-definition/pages/fern-definition/audiences.mdx +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: Audiences in Fern Definition -subtitle: Use audiences in your Fern Definition to segment your API for different groups of consumers. ---- - -Audiences are a useful tool for segmenting your API for different consumers. You can configure your Fern Docs to publish documentation specific to an `Audience`. You can use [audiences in your OpenAPI Specification](/learn/api-definition/openapi/audiences), too. - -Common examples of audiences include: - -- Internal consumers (e.g., frontend developers who use the API) -- Beta testers -- Customers - -By default, if no audience is specified, it will be accessible to all consumers. - -## Configuration - -The Fern Definition has a first-class concept for marking different endpoints, types, and properties for different audiences. - -To use audiences in your Fern Definition, add them to `api.yml`. - -In the example below, we have created audiences for `internal`, `beta`, and `customer` groups: - -```yaml title='api.yml' {2-5} -name: api -audiences: - - internal - - beta - - customers -``` - -## Audiences for endpoints - -To mark an endpoint for a particular consumer, add an `audience` with the relevant groups. - -In this example, the `sendEmail` endpoint is only available to internal consumers: - -```yaml title='user.yml' {6-7} -service: - base-path: /users - auth: true - endpoints: - sendEmail: - audiences: - - internal - path: /send-email - ... -``` - -## Audiences for types - -Types can also be marked for different audiences. - -In this example, the `Email` type is available to internal and beta consumers: - -```yaml title='user.yml' {5-7} -Email: - properties: - subject: string - body: optional - audiences: - - internal - - beta -``` - -## Audiences for properties - -Properties of a type can also be marked for different audiences. - -In this example, the `to` property is available to beta consumers only: - -```yaml title='user.yml' {8-9} -Email: - properties: - subject: string - body: optional - to: - type: string - docs: The recipient of the email - audiences: - - beta -``` - -## Audiences for SDKs - -In `generators.yml`, you can apply audience filters so that only certain -endpoints are passed to the generators. - -The following example configures the SDKs to filter for `customers`: - -```yaml title='generators.yml' {3-4} -groups: - external: - audiences: - - customers - generators: - ... -``` - -## Audiences with docs - -If generating Fern Docs, update your `docs.yml` configuration to include your audiences. - -The following example shows how to configure your `docs.yml` to publish documentation for the `customers` audience: - - -```yaml {3-4} -navigation: - - api: API Reference - audiences: - - customers -``` - diff --git a/fern/products/api-definition/pages/fern-definition/auth.mdx b/fern/products/api-definition/pages/fern-definition/auth.mdx deleted file mode 100644 index e75ebbbd4..000000000 --- a/fern/products/api-definition/pages/fern-definition/auth.mdx +++ /dev/null @@ -1,224 +0,0 @@ ---- -title: Authentication -subtitle: Model auth schemes such as bearer, basic, custom headers, and oauth. ---- - -Configuring authentication schemes happens in the `api.yml` file. - -```bash {5} -fern/ -├─ fern.config.json # root-level configuration -├─ generators.yml # generators you're using -└─ definition/ - ├─ api.yml # API-level configuration - └─ imdb.yml # endpoints, types, and errors -``` - -To add an authentication scheme, specify the authentication method under the `auth-schemes` section. - -```yaml api.yml {1-2} -auth-schemes: - AuthScheme: - ... -``` - - -To apply an authentication scheme across all endpoints, reference the `auth-scheme` within the `auth` section of your `api.yml` file. -```yaml api.yml {1} -auth: AuthScheme -auth-schemes: - AuthScheme: - ... -``` - - -## Bearer authentication - -Start by defining a `Bearer` authentication scheme in `api.yml`: - -```yaml api.yml -auth: Bearer -auth-schemes: - Bearer: - scheme: bearer -``` - -This will generate an SDK where the user would have to provide -a mandatory argument called `token`. - -```ts index.ts -const client = new Client({ - token: "ey34..." -}) -``` - -If you want to control variable naming and the environment variable to scan, -use the configuration below: - -```yaml title="api.yml" {5-7} -auth: Bearer -auth-schemes: - Bearer: - scheme: bearer - token: - name: apiKey - env: PLANTSTORE_API_KEY -``` - -The generated SDK would look like: - -```ts index.ts - -// Uses process.env.PLANTSTORE_API_KEY -let client = new Client(); - -// token has been renamed to apiKey -client = new Client({ - apiKey: "ey34..." -}) -``` - -## Basic authentication - -Start by defining a `Basic` authentication scheme in `api.yml`: - -```yaml api.yml -auth: Basic -auth-schemes: - Basic: - scheme: basic -``` - -This will generate an SDK where the user would have to provide -a mandatory arguments called `username` and `password`. - -```ts index.ts -const client = new Client({ - username: "joeschmoe" - password: "ey34..." -}) -``` - -If you want to control variable naming and environment variables to scan, -use the configuration below: - -```yaml title="api.yml" {5-11} -auth: Basic -auth-schemes: - Basic: - scheme: basic - username: - name: clientId - env: PLANTSTORE_CLIENT_ID - password: - name: clientSecret - env: PLANTSTORE_CLIENT_SECRET -``` - -The generated SDK would look like: - -```ts index.ts - -// Uses process.env.PLANTSTORE_CLIENT_ID and process.env.PLANTSTORE_CLIENT_SECRET -let client = new Client(); - -// parameters have been renamed -client = new Client({ - clientId: "joeschmoe", - clientSecret: "ey34..." -}) -``` - -## Custom header (e.g. API key) - -You can also create your own authentication scheme with customized headers. - -```yaml title="api.yml" {3-5} -auth: ApiKeyAuthScheme -auth-schemes: - ApiKeyAuthScheme: - header: X-API-Key - type: string -``` - -This will generate an SDK where the user would have to provide -a mandatory argument called `apiKey`. - -```ts index.ts -const client = new Client({ - xApiKey: "ey34..." -}) -``` - -If you want to control variable naming and environment variables to scan, -use the configuration below: - -```yaml title="api.yml" {7-8} -auth: ApiKeyAuthScheme -auth-schemes: - ApiKeyAuthScheme: - header: X-API-Key - type: string - name: apiKey - env: PLANTSTORE_API_KEY -``` - -The generated SDK would look like: - -```ts index.ts - -// Uses process.env.PLANTSTORE_API_KEY -let client = new Client(); - -// parameters have been renamed -client = new Client({ - apiKey: "ey34..." -}) -``` - -## OAuth client credentials - -If your API uses OAuth, you can specify an oauth scheme. Note that you'll need to define a token retrieval endpoint. - -```yaml api.yml -name: api - -imports: - auth: auth.yml - -auth: OAuthScheme -auth-schemes: - OAuthScheme: - scheme: oauth - type: client-credentials - client-id-env: YOUR_CLIENT_ID - client-secret-env: YOUR_CLIENT_SECRET - get-token: - endpoint: auth.getToken - response-properties: - access-token: $response.access_token - expires-in: $response.expires_in - -``` - -If the `expires-in` property is set, the generated OAuth token provider will automatically refresh the token when it expires. -Otherwise, it's assumed that the access token is valid indefinitely. - -With this, all of the OAuth logic happens automatically in the generated SDKs. As long as you configure these settings, your -client will automatically retrieve an access token and refresh it as needed. - -When using the docs playground, `token-header` and `token-prefix` can optionally be set to customize the header key name and -header value prefix, to match the expected format of the API auth scheme. - -For example, the following would produce a header `Fern-Authorization: Fern-Bearer `: - -```yaml api.yml {5-6} -auth-schemes: - OAuthScheme: - scheme: oauth - type: client-credentials - token-header: Fern-Authorization - token-prefix: Fern-Bearer - get-token: - ... -``` \ No newline at end of file diff --git a/fern/products/api-definition/pages/fern-definition/availability.mdx b/fern/products/api-definition/pages/fern-definition/availability.mdx deleted file mode 100644 index 9e335ec12..000000000 --- a/fern/products/api-definition/pages/fern-definition/availability.mdx +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: Availability in Fern Definition -description: Add availability to Fern Definition API services, endpoints, types, or properties to indicate their release status. ---- - -You can add `availability` to an endpoint, type, or property within your Fern Definition. - -Availability can be: -- `in-development` which means it is being worked on; will show a `Beta` tag -- `pre-release` which means it is available; will show a `Beta` tag -- `deprecated` which means it will be removed in the future; will show a `Deprecated` tag -- `generally-available` which means it is stable and available for use; will show a `GA` tag - -## Endpoint - - -```yaml {6} -service: - base-path: /pet - auth: true - endpoints: - add: - availability: deprecated - display-name: Add pet - docs: Add a new Pet to the store - method: POST - path: "" - request: AddPetRequest - response: Pet -``` - - -In Fern Docs, this will look like: - - -![Screenshot showing a deprecated tag next to an endpoint in API Reference docs](https://fern-image-hosting.s3.amazonaws.com/endpoint-deprecated.png) - - -## Type - - -```yaml {15} - Pet: - properties: - id: - type: integer - docs: A unique ID for the Pet - name: - type: string - docs: The first name of the Pet - photoUrls: - type: list - docs: A list of publicly available URLs featuring the Pet - availability: generally-available - category: - type: optional - availability: pre-release - - Category: - properties: - id: optional - name: optional -``` - - -In Fern Docs, this will look like: - - -![Screenshot showing a beta tag next to a type in API Reference docs](https://fern-image-hosting.s3.amazonaws.com/type-beta.png) - - -## Property - - -```yaml {12} - Pet: - properties: - id: - type: integer - docs: A unique ID for the Pet - name: - type: string - docs: The first name of the Pet - photoUrls: - type: list - docs: A list of publicly available URLs featuring the Pet - availability: deprecated - category: optional -``` - - -In Fern Docs, this will look like: - - -![Screenshot showing a deprecated tag next to a type's property in API Reference docs](https://fern-image-hosting.s3.amazonaws.com/property-deprecated.png) - diff --git a/fern/products/api-definition/pages/fern-definition/depending-on-other-apis.mdx b/fern/products/api-definition/pages/fern-definition/depending-on-other-apis.mdx deleted file mode 100644 index 5a327feaf..000000000 --- a/fern/products/api-definition/pages/fern-definition/depending-on-other-apis.mdx +++ /dev/null @@ -1,67 +0,0 @@ ---- -title: "Depending on other APIs" -subtitle: Import API Definitions to generate unified SDKs ---- - -Fern allows you to import other APIs into your API. - -This is often useful if: - -- you want to reuse another API's types in your API -- you want to combine multiple microservices' APIs into one SDK (similar to the AWS SDK) - -## Registering the dependency API - -The first step is to **register** the API you want to depend on. To do this, use -the `register` command: - -``` -$ fern register -[some-dependency]: Uploading definition... -[some-dependency]: Registered @fern/some-dependency:0.0.1 -``` - -## Depending on the registered API - -To add a dependency on another API, you must add a `dependencies.yml` to declare which -APIs you wish to depend on. - -```bash {4} -fern/ -├─ fern.config.json -├─ generators.yml -├─ dependencies.yml -└─ definition/ - ├─ api.yml - ├─ imdb.yml -``` - -Your `dependencies.yml` has a list of all the APIs you wish to depend: - -```yaml dependencies.yml -dependencies: - "@fern/some-dependency": "0.0.1" -``` - -Next, you need create a folder in your Fern Definition to house the dependency. Inside the folder, create a special file -`__package__.yml` which specifies the dependency and version you want to add. - -```bash {8-9} -fern/ -├─ fern.config.json -├─ generators.yml -├─ dependencies.yml -└─ definition/ - ├─ api.yml - ├─ imdb.yml - └─ my-folder - └─ __package__.yml -``` - -```yaml __package__.yml -export: - dependency: "@fern/some-dependency" -``` - -When you generate the SDK with `fern generate`, the `__package__.yml` file will -effectively be replaced with the API you're depending on. diff --git a/fern/products/api-definition/pages/fern-definition/endpoints.mdx b/fern/products/api-definition/pages/fern-definition/endpoints.mdx deleted file mode 100644 index ad91ab18f..000000000 --- a/fern/products/api-definition/pages/fern-definition/endpoints.mdx +++ /dev/null @@ -1,519 +0,0 @@ ---- -title: Endpoints in Fern Definition -description: Organize related API endpoints into a service in Fern Definition and define each endpoint's URL, HTTP method, request, response, errors, and more. ---- - -In Fern, you organize related endpoints into a **Service**. This grouping -improves clarity and makes the generated SDKs more idiomatic. - -## Service definition - -Each service defines: - -1. A **base-path**: A common prefix for all the endpoints' HTTP paths -2. Whether the service requires [authentication](/learn/api-definition/fern/authentication) -3. **Endpoints** - - - ```yaml - service: - base-path: /users - auth: false - endpoints: {} - ``` - - - - To define a service with an empty base path use the empty string: `base-path: ""` - - -## Endpoints - -An endpoint includes: - -- A **URL path** _(Optionally including path parameters)_ -- A **Display Name** _(Optional)_ -- An **HTTP Method** -- **Request information** _(Optional)_ - - **Query-parameters** - - **Headers** - - **Request body** -- **Successful (200) response** information _(Optional)_ -- **Error (non-200) responses** that this endpoint might return _(Optional)_ - -## URL path - -Each endpoint has a URL path. - - -```yaml {6} -service: - base-path: /users - auth: false - endpoints: - getAllUsers: - path: /all - method: GET -``` - - -The full path for the endpoint is the concatenation of: - -- The [environment](/learn/api-definition/fern/api-yml/environments) URL -- The service `base-path` -- The endpoint `path` - -## Display name - -The display name will appear as the title of an endpoint. By default, the display name is equal to the 'Title Case' of the endpoint name. If you would like to customize the endpoint name, you can **set the display name**. - -In the example below, ["Add a new plant to the store"](https://plantstore.dev/api-reference/plant/add-plant) displays as the title of the endpoint page within the API Reference. - - -```yaml {7} -service: - base-path: /v3 - auth: false - endpoints: - addPlant: - path: /plant - display-name: Add a new plant to the store - method: POST -``` - - -## Path parameters - -Supply path parameters for your endpoints to create dynamic URLs. - - -```yaml {6-8} -service: - base-path: /users - auth: false - endpoints: - getUser: - path: /{userId} - path-parameters: - userId: string - method: GET -``` - - -Services can also have path-parameters: - - - ```yaml {2-4} - service: - base-path: /projects/{projectId} - path-parameters: - projectId: string - auth: false - endpoints: - ... - ``` - - -## Query parameters - -Each endpoint can specify query parameters: - - -```yaml -service: - base-path: /users - auth: false - endpoints: - getAllUsers: - path: /all - method: GET - request: - # this name is required for idiomatic SDKs - name: GetAllUsersRequest - query-parameters: - limit: optional -``` - - -### `allow-multiple` - -Use `allow-multiple` to specify that a query parameter is allowed -multiple times in the URL, as in `?filter=jane&filter=smith`. This will alter -the generated SDKs so that consumers can provide multiple values for the query -parameter. - - -```yaml {5} - ... - query-parameters: - filter: - type: string - allow-multiple: true -``` - - -## Auth - -Each endpoint can override the auth behavior specified in the service. - - - ```yaml - service: - base-path: /users - auth: false - endpoints: - getMe: - path: "" - method: GET - # This endpoint will be authed - auth: true - docs: Return the current user based on Authorization header. - ``` - - -## Headers - -Each endpoint can specify request headers: - - - ```yaml - service: - base-path: /users - auth: false - endpoints: - getAllUsers: - path: /all - method: GET - request: - # this name is required for idiomatic SDKs name: - name: GetAllUsersRequest - headers: - X-Endpoint-Header: string - ``` - - -Services can also specify request headers. These headers will cascade to the service's endpoints. - - - ```yaml {4-5} - service: - base-path: /users - auth: false - headers: - X-Service-Header: string - endpoints: - getAllUsers: - path: /all - method: GET - request: - # this name is required for idiomatic SDKs - name: GetAllUsersRequest - headers: - X-Endpoint-Header: string - ``` - - -## Request body - -Endpoints can specify a request body type. - - -```yaml {10} -service: - base-path: /users - auth: false - endpoints: - setUserName: - path: /{userId}/set-name - path-parameters: - userId: string - method: POST - request: string -``` - - -### Inlining a request body - -If the request body is an object, you can **inline the type declaration**. This -makes the generated SDKs a bit more idiomatic. - - - ```yaml - service: - base-path: /users - auth: false - endpoints: - createUser: - path: /create - method: POST - request: - # this name is required for idiomatic SDKs - name: CreateUserRequest - body: - properties: - userName: string - ``` - - -## Success response - -Endpoints can specify a `response`, which is the type of the body that will be -returned on a successful (200) call. - - -```yaml -service: - base-path: /users - auth: false - endpoints: - getAllUsers: - path: /all - method: GET - response: list - -types: - User: - properties: - userId: string - name: string -``` - - -## Response status codes - -You can also use the `status-code` field to specify a custom status code -for a success response. - - -```yaml {11} -service: - base-path: /users - auth: false - endpoints: - create: : - path: "" - method: POST - request: CreateUserRequest - response: - type: User - status-code: 201 - -types: - User: - properties: - userId: string - name: string -``` - - -## Error responses - -Endpoints can specify error responses, which detail the non-200 responses that -the endpoint might return. - - -```yaml -service: - base-path: /users - auth: false - endpoints: - getUser: - path: /{userId} - path-parameters: - userId: string - method: GET - response: User - errors: - - UserNotFoundError - -types: - User: - properties: - userId: string - name: string - -errors: - UserNotFoundError: - status-code: 404 -``` - - -You can learn more about how to define errors on the [Errors](/learn/api-definition/fern/errors) page. - -## Specifying examples - -When you declare an example, you can also specify some examples of how that -endpoint might be used. These are used by the compiler to enhance the generated -outputs. Examples will show up as comments in your SDKs, API documentation, and Postman collection. - -You may add examples for endpoints, types, and errors. - - -```yaml {13-19} -service: - base-path: /users - auth: false - endpoints: - getUser: - path: /{userId} - path-parameters: - userId: string - method: GET - response: User - errors: - - UserNotFoundError - examples: - - path-parameters: - userId: alice-user-id - response: - body: - userId: alice-user-id - name: Alice - -types: - User: - properties: - userId: string - name: string - -errors: - UserNotFoundError: - status-code: 404 -``` - - -If you're adding an example to an endpoint and the type already has an example, you can reference it using `$`. -```yaml -service: - auth: true - base-path: /address - endpoints: - create: - method: POST - path: "" - request: CreateAddress - response: Address - examples: - - request: $CreateAddress.WhiteHouse - response: - body: $Address.WhiteHouseWithID - - CreateAddress: - properties: - street1: string - street2: optional - city: string - state: string - postalCode: string - country: string - isResidential: boolean - examples: - - name: WhiteHouse - value: - street1: 1600 Pennsylvania Avenue NW - city: Washington DC - state: Washington DC - postalCode: "20500" - country: US - isResidential: true - - Address: - extends: CreateAddress - properties: - id: - type: uuid - docs: The unique identifier for the address. - examples: - - name: WhiteHouseWithID - value: - id: 65ce514c-41e3-11ee-be56-0242ac120002 - street1: 1600 Pennsylvania Avenue NW - city: Washington DC - state: Washington DC - postalCode: "20500" - country: US - isResidential: true -```` - -Examples contain all the information about the endpoint call, including -the request body, path parameters, query parameters, headers, and response body. - - - ```yaml - examples: - - path-parameters: - userId: some-user-id - query-parameters: - limit: 50 - headers: - X-My-Header: some-value - response: - body: - response-field: hello - ``` - - -### Failed examples - -You can also specify examples of failed endpoints calls. Add the `error` -property to a response example to designate which failure you're demonstrating. - - -```yaml {5} -examples: - - path-parameters: - userId: missing-user-id - response: - error: UserNotFoundError - -errors: - UserNotFoundError: - status-code: 404 -``` - - -If the error has a body, then you must include the body in the example. - - -```yaml {6, 11} -examples: - - path-parameters: - userId: missing-user-id - response: - error: UserNotFoundError - body: "User with id `missing-user-id` was not found" - -errors: - UserNotFoundError: - status-code: 404 - type: string -``` - - -### Referencing examples from types - -To avoid duplication, you can reference examples from types using `$`. - - -```yaml {12} -service: - base-path: /users - auth: true - endpoints: - getUser: - method: GET - path: /{userId} - path-parameters: - userId: UserId - examples: - - path-parameters: - userId: $UserId.Example1 - -types: - UserId: - type: integer - examples: - - name: Example1 - value: user-id-123 -``` - diff --git a/fern/products/api-definition/pages/fern-definition/endpoints/bytes.mdx b/fern/products/api-definition/pages/fern-definition/endpoints/bytes.mdx deleted file mode 100644 index 43a1926d8..000000000 --- a/fern/products/api-definition/pages/fern-definition/endpoints/bytes.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Binary Data and Files -subtitle: Use the `bytes` type to handle binary data in your API ---- - - - The `bytes` type allows you to handle binary data in both requests and responses. - - -## Sending bytes - -If your API needs to send a stream of bytes (i.e. typical for assets like audio, images and other files) then -you can use the `bytes` type in the Fern Definition to model this. - -```yml audio.yml -service: - base-path: /audio - endpoints: - upload: - display-name: Upload audio - method: POST - path: /upload - content-type: application/octet-stream - request: - type: bytes - docs: The bytes of the MP3 file that you would like to upload -``` - -## Receiving bytes - - - When handling binary data in responses, use `type: file` instead of `type: bytes`. - - -On the other hand, if your API is returning a stream of bytes, then you can leverage the `bytes` type as a response. - -```yml textToSpeech.yml -service: - base-path: /tts - endpoints: - upload: - display-name: Upload audio - method: POST - path: "" - request: - name: TTSRequest - body: - properties: - text: - type: string - docs: The text that you want converted to speech. - response: - type: file - docs: The bytes of the audio file. -``` - - - - - diff --git a/fern/products/api-definition/pages/fern-definition/endpoints/multipart.mdx b/fern/products/api-definition/pages/fern-definition/endpoints/multipart.mdx deleted file mode 100644 index f5270d6ca..000000000 --- a/fern/products/api-definition/pages/fern-definition/endpoints/multipart.mdx +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: Multipart File Upload -description: Document endpoints with the `multiform` content type. ---- - -Endpoints in Fern are defined underneath the `endpoints` key. If your endpoint request includes file uploads, you can use the `file` type to indicate the request is of a `multiform` content type. The example below demonstrates an endpoint which includes a file in the request body. - - -```yaml {12} -service: - base-path: /documents - auth: false - endpoints: - uploadDocument: - path: /upload - method: POST - request: - name: UploadDocumentRequest - body: - properties: - file: file -``` - - -Within a given multipart request, a string parameter with `format:binary` will represent an arbitrary file. - -## List of Files - -If your endpoint supports a list of files, then your request body must indicate such. - - -```yaml {12} -service: - base-path: /documents - auth: false - endpoints: - uploadDocuments: - path: /upload - method: POST - request: - name: UploadDocumentsRequest - body: - properties: - files: list -``` - diff --git a/fern/products/api-definition/pages/fern-definition/endpoints/rest.mdx b/fern/products/api-definition/pages/fern-definition/endpoints/rest.mdx deleted file mode 100644 index 3a4459ba2..000000000 --- a/fern/products/api-definition/pages/fern-definition/endpoints/rest.mdx +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: HTTP JSON Endpoints ---- - -Endpoints in Fern are defined underneath the `endpoints` key. Below is an example of defining -a single REST endpoint: - -```yml title="users.yml" maxLines=0 -service: - base-path: /users - auth: false - endpoints: - createUser: - path: /create - method: POST - request: - body: - properties: - userName: string -``` - -## Examples - -You can provide examples of requests and responses by using the `examples` key. - -```yaml {11-17} -service: - base-path: /users - auth: false - endpoints: - getUser: - path: /{userId} - path-parameters: - userId: string - method: GET - response: User - examples: - - path-parameters: - userId: alice-user-id - response: - body: - userId: alice-user-id - name: Alice -``` - diff --git a/fern/products/api-definition/pages/fern-definition/endpoints/sse.mdx b/fern/products/api-definition/pages/fern-definition/endpoints/sse.mdx deleted file mode 100644 index c1ab53770..000000000 --- a/fern/products/api-definition/pages/fern-definition/endpoints/sse.mdx +++ /dev/null @@ -1,101 +0,0 @@ ---- -title: Server-Sent Events and Streaming APIs -subtitle: Use the `response-stream` key to model streaming endpoints ---- - - - Specifying `response-stream` on an endpoints allows you to represent endpoint responses that are streaming. - - - -## JSON streaming - -If your API returns a series of `JSON` chunks as seen below - -```json -{ "text": "Hi, I am a" } -{ "text": "chatbot. Do you have any"} -{ "text": "questions for me"} -``` - -then simply specify the response under `response-stream` for your endpoint. - -```yaml title="chat.yml" {4} -service: - base-path: /chat - endpoints: - stream: - method: POST - path: "" - response-stream: Chat - -types: - Chat: - properties: - text: string -``` - -## Server-sent events - -If your API returns server-sent-events, with the `data` and `event` keys as seen below - -```json -data: { "text": "Hi, I am a" } -data: { "text": "chatbot. Do you have any"} -data: { "text": "questions for me"} -``` - -then make sure to include `format: sse`. - -```yaml title="chat.yml" {9} -service: - base-path: /chat - endpoints: - stream: - method: POST - path: "" - response-stream: - type: Chat - format: sse - -types: - Chat: - properties: - text: string -``` - -## `Stream` parameter - -It has become common practice for endpoints to have a `stream` parameter that -controls whether the response is streamed or not. Fern supports this pattern in a first -class way. - -Simply specify the `stream-condition` as well as the ordinary response and the streaming response: - -```yaml title="chat.yml" {7} -service: - base-path: /chat - endpoints: - stream: - method: POST - path: "" - stream-condition: $request.stream - request: - name: StreamChatRequest - body: - properties: - stream: boolean - response: Chat - response-stream: - type: ChatChunk - format: sse - -types: - Chat: - properties: - text: string - tokens: integer - ChatChunk: - properties: - text: string -``` \ No newline at end of file diff --git a/fern/products/api-definition/pages/fern-definition/errors.mdx b/fern/products/api-definition/pages/fern-definition/errors.mdx deleted file mode 100644 index 60fa6e63a..000000000 --- a/fern/products/api-definition/pages/fern-definition/errors.mdx +++ /dev/null @@ -1,25 +0,0 @@ ---- -title: Errors in Fern Definition -description: Add errors representing failed responses from API endpoints in Fern Definition. ---- - -Errors represent failed (non-200) responses from endpoints. - -An error has: - -- An HTTP status code -- A body type _(Optional)_ - - -```yaml -errors: - UserNotFoundError: - status-code: 404 - type: UserNotFoundErrorBody - -types: - UserNotFoundErrorBody: - properties: - requestedUserId: string -``` - \ No newline at end of file diff --git a/fern/products/api-definition/pages/fern-definition/examples.mdx b/fern/products/api-definition/pages/fern-definition/examples.mdx deleted file mode 100644 index b16ab4936..000000000 --- a/fern/products/api-definition/pages/fern-definition/examples.mdx +++ /dev/null @@ -1,339 +0,0 @@ ---- -title: Examples in Fern Definition -subtitle: Use Fern Definition to add API examples that are shown in comments of SDKs, API Reference documentation, and a Postman collection. ---- - -You can add examples for types and endpoints. Examples are shown as -comments in your SDKs, in the request & response of your documentation, -and in a Postman Collection. - -## Validation - -The Fern CLI validates that your examples match the expected types. The following won't compile: - -```yaml -types: - UserId: - type: integer - examples: - - value: hello # not an integer -``` - -```bash CLI Error Message -[api]: example.yml -> types -> UserId -> examples[0] - Expected example to be an integer. Example is: "hello" -``` - -## Referencing examples - -You can reference an example from another type, endpoint, or error. - -Just like types, you can compose examples. To reference an example from another -type, use `$`. - -```yaml {14} -types: - UserId: - type: integer - examples: - - name: Example1 - value: user-id-123 - - User: - properties: - id: UserId - name: string - examples: - - value: - id: $UserId.Example1 - name: Jane Smith -``` - -## Examples for types - -### Objects - -```yml -types: - ShipTo: - properties: - street1: string - street2: optional - city: string - state: string - postalCode: string - country: Country - isResidential: boolean - examples: - - name: WhiteHouse - value: - street1: 1600 Pennsylvania Avenue NW - city: Washington DC - state: Washington DC - postalCode: "20500" - country: US - isResidential: true - - name: EmpireStateBuilding - value: - street1: 350 5th Ave - street2: Attn: Maintenance Department - city: New York - state: NY - postalCode: "10118" - country: US - isResidential: false -``` - - -```typescript -/** - * Represents a shipping address. - * - * The White House address - * @example { - * street1: "1600 Pennsylvania Avenue NW", - * city: "Washington DC", - * state: "Washington DC", - * postalCode: "20500", - * country: "US", - * isResidential: true - * } - * - * * The Empire State Building address - * @example { - * street1: "350 5th Ave", - * street2: "Attn: Maintenance Department", - * city: "New York", - * state: "NY", - * postalCode: "10118", - * country: "US", - * isResidential: false - * } - */ -type ShipTo = { - street1: string; - street2?: string; - city: string; - state: string; - postalCode: string; - country: Country; - isResidential: boolean; -}; -``` - - -### Lists - -```yml - Shipments: - type: list - examples: - - name: Default - value: - - status: "InTransit" - estimatedDeliveryDate: "2024-01-11" - - status: "Delivered" - estimatedDeliveryDate: "2024-01-13" -``` - -### Unions - -#### Discriminated union - -```yml -types: - Animal: - union: - dog: Dog - cat: Cat - examples: - - value: - type: dog - likesToWoof: true - Dog: - properties: - likesToWoof: boolean - Cat: - properties: - likesToMeow: boolean -``` - - -```typescript -/** - * Represents an animal, which can be either a Dog or a Cat. - * - * Example of a Dog: - * @example { - * type: "dog", - * likesToWoof: true - * } - */ -type Animal = Dog | Cat; -``` - - -#### Undiscriminated union - -```yml -types: - Animal: - discriminated: false - union: - - Dog - - Cat - examples: - - value: - likesToMeow: true - Dog: - properties: - likesToWoof: boolean - Cat: - properties: - likesToMeow: boolean -``` - - -```typescript -/** - * Represents an Animal, which can be either a Dog or a Cat. - * - * Example of an Animal as a Cat: - * @example { - * likesToMeow: true - * } - */ -type Animal = Dog | Cat; -``` - - -### Aliases - -```yml -types: - UserId: - docs: A unique identifier for a user - type: string - examples: - - value: user-id-123 -``` - - - ```typescript - /** - * A unique identifier for a user * - * @example "user-id-123" - */ - type UserId = string; - ``` - - -## Examples for endpoints - -You can add examples of successful and error responses for your endpoints. -Examples can reference the examples of types to avoid duplication. - -```yml -service: - auth: true - base-path: "" - endpoints: - CreateShippingLabel: - docs: Create a new shipping label. - method: POST - path: /shipping - request: CreateShippingLabelRequest - response: ShippingLabel - errors: - - NotAuthorized - - InsufficientFunds - examples: - # A successful response that doesn't reference other examples. - - request: - orderId: "online_789" - weightInOunces: 5 - response: - body: - orderId: "online_789" - weightInOunces: 5 - trackingNumber: "1Z26W8370303469306" - price: 2.50 - - # A successful response that uses references. - - request: $CreateShippingLabelRequest.SuccessfulRequest - response: - body: $ShippingLabel.Default - - # An error response. - - request: $CreateShippingLabelRequest.InsufficientFundsRequest - response: - error: InsufficientFunds - body: $InsufficientFundsBody.Default - -types: - CreateShippingLabelRequest: - properties: - orderId: string - weightInOunces: integer - examples: - - name: SuccessfulRequest - value: - orderId: "online_123" - weightInOunces: 13 - - name: InsufficientFundsRequest - value: - orderId: "online_456" - weightInOunces: 2000 - - ShippingLabel: - properties: - orderId: string - weightInOunces: integer - trackingNumber: string - price: double - examples: - - name: Default - value: - orderId: "online_123" - weightInOunces: 13 - trackingNumber: "1Z12345E0205271688" - price: 12.35 - - InsufficientFundsBody: - properties: - message: string - examples: - - name: Default - value: - message: "Insufficient funds to create shipping label." - -errors: - NotAuthorized: - status-code: 401 - InsufficientFunds: - status-code: 422 - type: InsufficientFundsBody -``` - -## Examples for path parameters - -```yml -service: - auth: true - base-path: "" - endpoints: - TrackShipment: - docs: Track the status of a shipment. - method: GET - path: /shipping/{trackingNumber} - path-parameters: - trackingNumber: string - response: ShipmentStatus - examples: - - path-parameters: - trackingNumber: "1Z26W8370303469306" - response: - body: - status: "InTransit" - estimatedDeliveryDate: "2024-01-11" -``` diff --git a/fern/products/api-definition/pages/fern-definition/export-openapi.mdx b/fern/products/api-definition/pages/fern-definition/export-openapi.mdx deleted file mode 100644 index cf5b99cd4..000000000 --- a/fern/products/api-definition/pages/fern-definition/export-openapi.mdx +++ /dev/null @@ -1,23 +0,0 @@ ---- -title: Export from Fern Definition to OpenAPI -description: Export your Fern Definition files to OpenAPI using Fern's OpenAPI generator. ---- - -To prevent lock-in to the Fern Definition format, we provide a generator that will export your Fern Def files to OpenAPI 3.1. -This lets you switch to using OpenAPI at any time, or use your API definition with OpenAPI tools. -To convert your Fern Definition to OpenAPI, use the `fern-openapi` generator. - -Update your `generators.yml` file: - - -```yaml -- name: fernapi/fern-openapi - version: 0.0.31 - config: - format: yaml # options are yaml or json - output: - location: local-file-system - path: ../openapi # relative path to output location -``` - - diff --git a/fern/products/api-definition/pages/fern-definition/imports.mdx b/fern/products/api-definition/pages/fern-definition/imports.mdx deleted file mode 100644 index d52749f86..000000000 --- a/fern/products/api-definition/pages/fern-definition/imports.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -title: Imports in Fern Definition -description: Use imports to reference API types and errors from other Fern Definition files. ---- - -Imports allow you to reference types and errors from other files. - -```yaml title="person.yml" -types: - Person: ... -``` - -```yaml title="family.yml" -imports: - person: ./path/to/person.yml -types: - Family: - properties: - people: list # use an imported type -``` - -Note that you can only import files that exist in your Fern Definition (i.e., in the same `definition/` folder). diff --git a/fern/products/api-definition/pages/fern-definition/overview.mdx b/fern/products/api-definition/pages/fern-definition/overview.mdx deleted file mode 100644 index 25b04a2db..000000000 --- a/fern/products/api-definition/pages/fern-definition/overview.mdx +++ /dev/null @@ -1,104 +0,0 @@ ---- -title: What is a Fern Definition? -subtitle: "A Fern Definition is a set of YAML files that describe your API." ---- - -A Fern Definition is a set of YAML files that are the single source of truth for your API. You check your Fern Definition into your repo, -inside of which describes your API requests, responses, models, paths, methods, errors, and authentication scheme. - - - Want to use OpenAPI instead? No worries, we support that [as well](/learn/api-definition/introduction/what-is-an-api-definition#openapi-swagger) - - -## Fern Definition structure - -To initialize a Fern Definition, simply run: - -```sh -npm install -g fern-api -fern init -``` - -This will create the following folder structure in your project: - -```bash -fern/ -├─ fern.config.json # root-level configuration -├─ generators.yml # generators you're using -└─ definition/ - ├─ api.yml # API-level configuration - └─ imdb.yml # endpoints, types, and errors -``` - -## Definition file - -Each **Fern Definition** file may define: - -- **[Custom types](/learn/api-definition/fern/types)**. Use **custom types** to build your data model. -- **[Endpoints](/learn/api-definition/fern/endpoints)**. A **service** is a set of related REST endpoints. -- **[Errors](/learn/api-definition/fern/errors)**. An **error** represents a failed (non-200) response from an endpoint. -- **[Imports](/learn/api-definition/fern/imports)**. Use **imports** to share types across files. - -```yml imdb.yml maxLines=0 -service: - auth: false - base-path: /movies - endpoints: - createMovie: - docs: Add a movie to the database - method: POST - path: /create-movie - request: CreateMovieRequest - response: MovieId - - getMovie: - method: GET - path: /{movieId} - path-parameters: - movieId: MovieId - response: Movie - errors: - - NotFoundError - - UnauthorizedError - -types: - Movie: - properties: - title: string - rating: - type: double - docs: The rating scale from one to five stars - id: - type: MovieId - docs: The unique identifier for a movie - - CreateMovieRequest: - properties: - title: string - rating: double - -errors: - NotFoundError: - http: - statusCode: 404 - type: - properties: - id: MovieId - - UnauthorizedError: - http: - statusCode: 401 -``` - -## Why another format? - -Google built gRPC. Amazon built Smithy. Facebook built GraphQL. Palantir built -Conjure. These companies rejected OpenAPI in favor of a more concise API Definition Language. - -We built Fern to productize this design and make it accessible to all -software companies. - - - Despite being a different format for describing APIs, **you are never locked in to Fern.** It's easy to convert your - [Fern Definition to OpenAPI](/learn/api-definition/fern/export-openapi). - diff --git a/fern/products/api-definition/pages/fern-definition/packages.mdx b/fern/products/api-definition/pages/fern-definition/packages.mdx deleted file mode 100644 index d66564568..000000000 --- a/fern/products/api-definition/pages/fern-definition/packages.mdx +++ /dev/null @@ -1,153 +0,0 @@ ---- -title: Packages in Fern Definition -description: Fern Definition enables the reuse of API type and error names across packages, and can configure the structure of your API documentation. ---- - -## What is a package? - -Every folder in your API definition is a package. - - -```bash -fern/ -├─ fern.config.json -├─ generators.yml -└─ definition/ # <--- root package - ├─ api.yml - ├─ projects.yml - └─ roles/ # <--- nested package - └─ admin.yml -``` - - -The generated SDK will match the hierarchy of your API definition. - - -```ts -const client = new Client(); - -// calling endpoint defined in projects.yml -client.projects.get(); - -// calling endpoint defined in roles/admin.yml -client.roles.admin.get(); -``` - - -## Package configuration - -Each package can have a special definition file called `__package__.yml`. Like any -other definition file, it can contain [imports](/learn/api-definition/fern/imports), -[types](/learn/api-definition/fern/types), [endpoints](/learn/api-definition/fern/endpoints), -and [errors](/learn/api-definition/fern/errors). - -Endpoints in `__package__.yml` will appear at the root of the package. -For example, the following generated SDK: - - -```ts -const client = new Client(); - -client.getProjects(); -``` - - -would have a `fern/` folder: - - -```bash {5} -fern/ -├─ fern.config.json -├─ generators.yml -└─ definition/ - ├─ __package__.yml - └─ roles.yml -``` - - -that contains the following `__package__.yml`: - - -```yaml -service: - base-path: "" - auth: false - endpoints: - getProjects: - method: GET - path: "" - response: list -``` - - -## Namespacing - -Each package has its own namespace. This means you can reuse type names and -error names across packages. - -This is useful when versioning your APIs. For example, when you want to -increment your API version, you can copy the existing API -to a new package and start making changes. If the new API version reuses -certain types or errors, that's okay because the two APIs live in different -packages. - - -```bash -fern/ -├─ fern.config.json -├─ generators.yml -└─ definition/ - ├─ api.yml - └─ roles/ - └─ v1/ - └─ admin.yml # type names can overlap with v2/admin.yml - └─ v2/ - └─ admin.yml -``` - - -## Navigation - -`__package__.yml` also allows you to configure the navigation order -of your services. This is relevant when you want to control the display -of your documentation. - -For example, let's say you have the following `fern/` folder: - - -```bash -fern/ -├─ fern.config.json -├─ generators.yml -└─ definition/ - ├─ projects.yml - ├─ roles.yml - └─ users.yml -``` - - -Your API will be sorted alphabetically: projects, roles, then users. If you -want to control the navigation, you can add a `__package__.yml` file -and configure the order: - - -```bash -fern/ -├─ fern.config.json -├─ generators.yml -└─ definition/ - ├─ __package__.yml # <--- New File - ├─ projects.yml - ├─ roles.yml - └─ users.yml -``` - - - -```yaml -navigation: - - users.yml - - roles.yml - - projects.yml -``` - \ No newline at end of file diff --git a/fern/products/api-definition/pages/fern-definition/types.mdx b/fern/products/api-definition/pages/fern-definition/types.mdx deleted file mode 100644 index 894f06b53..000000000 --- a/fern/products/api-definition/pages/fern-definition/types.mdx +++ /dev/null @@ -1,279 +0,0 @@ ---- -title: Types in Fern Definition -description: Types describe the data model of your API. Fern has many built-in types and supports custom types, as well as extending and aliasing objects, and unions. ---- - -Types describe the data model of your API. - -## Built-in types - -- `string` -- `integer` -- `long` -- `double` -- `boolean` -- `datetime` _An [RFC 3339, section 5.6 datetime](https://ijmacd.github.io/rfc3339-iso8601/). For example, `2017-07-21T17:32:28Z`._ -- `date` _An RFC 3339, section 5.6 date (YYYY-MM-DD). For example, `2017-07-21`._ -- `uuid` -- `base64` -- `list` _e.g., list\_ -- `set` _e.g., set\_ -- `map` _e.g., map\_ -- `optional` _e.g., optional\_ -- `literal` _e.g., literal\<"Plants"\>_ -- `file` _e.g., [file uploads](/learn/api-definition/fern/endpoints/multipart)_ -- `unknown` _Represents arbitrary JSON._ - -## Custom types - -Creating your own types is easy in Fern! - -### Objects - -The most common custom types are **objects**. - -In Fern, you use the `"properties"` key to create an object: - -```yaml {3,8} -types: - Person: - properties: - name: string - address: Address - - Address: - properties: - line1: string - line2: optional - city: string - state: string - zip: string - country: literal<"USA"> -``` - -These represent JSON objects: - -```json -{ - "name": "Alice", - "address": { - "line1": "123 Happy Lane", - "city": "New York", - "state": "NY", - "zip": "10001", - "country": "USA" - } -} -``` - -You can also use **extends** to compose objects: - -```yaml {6} -types: - Pet: - properties: - name: string - Dog: - extends: Pet - properties: - breed: string -``` - -You can extend multiple objects: - -```yaml {3-5} -types: - GoldenRetriever: - extends: - - Dog - - Pet - properties: - isGoodBoy: boolean -``` - -### Aliases - -An Alias type is a renaming of an existing type. This is usually done for clarity. - -```yaml -types: - # UserId is an alias of string - UserId: string - - User: - properties: - id: UserId - name: string -``` - -### Enums - -An enum represents a string with a set of allowed values. - -In Fern, you use the `"enum"` key to create an enum: - -```yaml {3} -types: - WeatherReport: - enum: - - SUNNY - - CLOUDY - - RAINING - - SNOWING -``` - -Enum names are restricted to `A-Z`, `a-z`, `0-9`, and `_` to ensure that generated code can compile across all of the languages that Fern can output. If you have an enum that doesn't follow this convention, you can use the `"name"` key to specify a custom name: - -```yaml -types: - Operator: - enum: - - name: LESS_THAN # <--- the name that will be used in SDKs - value: '<' # <--- the value that will be serialized - - name: GREATER_THAN - value: '>' - - name: NOT_EQUAL - value: '!=' -``` - -### Discriminated Unions - -Fern supports tagged unions (a.k.a. discriminated unions). Unions are useful for -polymorphism. This is similar to the `oneOf` concept in OpenAPI. - -In Fern, you use the `"union"` key to create an union: - -```yaml {3-5} -types: - Animal: - union: - dog: Dog - cat: Cat - Dog: - properties: - likesToWoof: boolean - Cat: - properties: - likesToMeow: boolean -``` - -In JSON, unions have a **discriminant property** to differentiate between -different members of the union. By default, Fern uses `"type"` as the -discriminant property: - -```json -{ - "type": "dog", - "likesToWoof": true -} -``` - -You can customize the discriminant property using the "discriminant" key: - -```yaml {3} - types: - Animal: - discriminant: animalType - union: - dog: Dog - cat: Cat - Dog: - properties: - likesToWoof: boolean - Cat: - properties: - likesToMeow: boolean -``` - -This corresponds to a JSON object like this: - -```json -{ - "animalType": "dog", - "likesToWoof": true -} -``` - -### Undiscriminated Unions - -Undiscriminated unions are similar to discriminated unions, however you don't -need to define an explicit discriminant property. - -```yaml -MyUnion: - discriminated: false - union: - - string - - integer -``` - -### Generics - -Fern supports shallow generic objects, to minimize code duplication. You can -define a generic for reuse like so: - -```yaml -MySpecialMapItem: - properties: - key: Key, - value: Value, - diagnostics: string -``` - -Now, you can instantiate generic types as a type alias: - -```yml -StringIntegerMapItem: - type: Response - -StringStringMapItem: - type: Response -``` - -You can now freely use this type as if it were any other type! Note, generated -code will not use generics. The above example will be generated in typescript as: - -```typescript -type StringIntegerMapItem = { - key: string, - value: number, - diagnostics: string -} - -type StringStringMapItem = { - key: string, - value: string, - diagnostics: string -} -``` - -### Documenting types - -You can add documentation for types. These docs are passed into the compiler, -and are incredibly useful in the generated outputs (e.g., docstrings in SDKs). - - -```yaml -types: - Person: - docs: A person represents a human being - properties: - name: string - age: - docs: age in years - type: integer -``` - - - -```typescript -/** - * A person represents a human being - */ -interface Person { - name: string; - // age in years - age: number; -} -``` - diff --git a/fern/products/api-definition/pages/fern-definition/webhooks.mdx b/fern/products/api-definition/pages/fern-definition/webhooks.mdx deleted file mode 100644 index 399c3c2a1..000000000 --- a/fern/products/api-definition/pages/fern-definition/webhooks.mdx +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Webhooks in the Fern Definition -description: Learn how to define webhooks in the Fern Definition ---- - -In Fern, you can specify webhooks in your API definition. The webhooks will be included -in both the generated SDKs and the API documentation. - -## Webhook definition - -Each webhook defines: - -1. **Method**: The HTTP Method that the webhook will use (either `GET` or `POST`) -2. **Headers**: The headers that the webhook will send -3. **Payload**: The schema of the webhook payload - - - ```yaml {2-10} - webhooks: - paymentNotification: - display-name: Payment Notification - docs: Receive a notification when a payment changes status - method: POST - headers: - X-Signature-Primary: - type: string - docs: An HMAC signature of the payload - payload: PaymentNotificationPayload - - types: - PaymentNotificationPayload: - discriminant: notificationType - union: - queued: QueuedPaymentNotification - processing: ProcessingPaymentNotification - completed: CompletedPaymentNotification - ``` - - -### Inlined payloads - -You can inline the schema of the payload by doing the following: - - - ```yaml - webhooks: - paymentNotification: - display-name: Payment Notification - docs: Receive a notification when a payment changes status - method: POST - headers: - X-Signature-Primary: - type: string - docs: An HMAC signature of the payload - payload: - name: PaymentNotificationPayload - properties: - id: - type: string - docs: The notification id - amount: double - currency: Currency - ``` - - diff --git a/fern/products/api-definition/pages/fern-definition/websocket.png b/fern/products/api-definition/pages/fern-definition/websocket.png deleted file mode 100644 index e7f3cc355..000000000 Binary files a/fern/products/api-definition/pages/fern-definition/websocket.png and /dev/null differ diff --git a/fern/products/api-definition/pages/fern-definition/websockets.mdx b/fern/products/api-definition/pages/fern-definition/websockets.mdx deleted file mode 100644 index efac1a6e0..000000000 --- a/fern/products/api-definition/pages/fern-definition/websockets.mdx +++ /dev/null @@ -1,96 +0,0 @@ ---- -title: WebSockets in the Fern Definition -description: Learn how to define WebSockets in the Fern Definition ---- - -WebSockets enable a user to create a connection with a server, over which bidirectional communication can be sent. - -In Fern, you can specify WebSockets in your API definition. The WebSockets will be included in both the generated SDKs and the API documentation. - -## WebSocket definition -Each WebSocket is defined in its own file, where it is described by the `channel` object. - -### The channel object - -A `channel` is defined by the following fields: - -- `auth`: The authentication scheme for the WebSocket -- `path`: The path of the WebSocket -- `headers` _(Optional)_: Any headers the WebSocket will send -- `path-parameters` _(Optional)_: Any path parameters in the WebSocket path -- `query-parameters` _(Optional)_: Any query parameters used in the initial request of the WebSocket -- `messages` _(Optional)_: The schemas of the messages the WebSocket can send and receive once connected - - `origin`: The entity that sent the message (e.g. `client` or `server`) - - `body`: The schema of the message -- `examples`: Example WebSocket connection _(Optional)_ - -### WebSocket example - - - ```yaml - channel: - path: /chat - auth: false - query-parameters: - model_id: - type: optional - docs: The unique identifier of the model. - model_version: - type: optional - docs: The version number of the model. - messages: - publish: - origin: client - body: PublishEvent - subscribe: - origin: server - body: SubscribeEvent - examples: - - query-parameters: - model_id: "123" - messages: - - type: publish - body: - text: "Hello, world." - - type: subscribe - body: - id: "23823049" - message: "Hello there, how are you?" - types: - PublishEvent: - docs: The input from the user to send through the WebSocket. - properties: - text: - type: string - docs: The user text to send into the conversation. - SubscribeEvent: - docs: The response from the server sent through the WebSocket. - properties: - id: - type: string - docs: The id of the message. - message: - type: string - docs: The message sent through the socket. - ``` - - -## WebSocket API Reference - -### WebSocket Reference - -Fern renders a unique reference page for WebSockets. The **Handshake** section outlines the protocol for connecting with the server, while the **Send** and **Receive** sections outline the message schemas that can be sent between the client and server. - - -The WebSocket Reference - - -### WebSocket Playground - - - -Users can connect to and use WebSockets from right within the API Reference (check one of Hume's WebSockets [here](https://dev.hume.ai/reference/empathic-voice-interface-evi/chat/chat)). - - -WebSocket Playground - \ No newline at end of file diff --git a/fern/products/api-definition/pages/fern-definition/wss-reference.png b/fern/products/api-definition/pages/fern-definition/wss-reference.png deleted file mode 100644 index cfef31111..000000000 Binary files a/fern/products/api-definition/pages/fern-definition/wss-reference.png and /dev/null differ diff --git a/fern/products/api-definition/pages/introduction/what-is-an-api-definition.mdx b/fern/products/api-definition/pages/introduction/what-is-an-api-definition.mdx deleted file mode 100644 index 1c0d973ca..000000000 --- a/fern/products/api-definition/pages/introduction/what-is-an-api-definition.mdx +++ /dev/null @@ -1,299 +0,0 @@ ---- -title: What is an API Definition? -description: Describes the contract between the API provider and API consumer ---- - - -An API Definition is a document that defines the structure of the API. It includes the **endpoints**, -**request and response schemas**, and **authentication** requirements. - - -Fern integrates with several API definition formats: - - - - Formerly known as Swagger, [OpenAPI](https://swagger.io/specification/) is the most popular API definition format. - OpenAPI can be used to document RESTful APIs and is defined in a YAML or JSON file. - - Check out an example OpenAPI Specification for the Petstore API [here](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml) - - ```yaml maxLines={0} - openapi: 3.0.2 - tags: - - name: pet - description: Everything about your Pets - paths: - /pet: - post: - tags: - - pet - summary: Add a new pet to the store - description: Add a new pet to the store - operationId: addPet - requestBody: - description: Create a new pet in the store - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - application/xml: - schema: - $ref: '#/components/schemas/Pet' - application/x-www-form-urlencoded: - schema: - $ref: '#/components/schemas/Pet' - responses: - '200': - description: Successful operation - content: - application/xml: - schema: - $ref: '#/components/schemas/Pet' - application/json: - schema: - $ref: '#/components/schemas/Pet' - '405': - description: Invalid input - components: - schemas: - Pet: - required: - - name - - photoUrls - properties: - id: - type: integer - format: int64 - example: 10 - name: - type: string - example: doggie - category: - $ref: '#/components/schemas/Category' - photoUrls: - type: array - xml: - wrapped: true - items: - type: string - xml: - name: photoUrl - tags: - type: array - xml: - wrapped: true - items: - $ref: '#/components/schemas/Tag' - xml: - name: tag - status: - type: string - description: pet status in the store - enum: - - available - - pending - - sold - xml: - name: pet - type: object - ``` - - - [AsyncAPI](https://v2.asyncapi.com/docs) is a specification for defining event-driven APIs. It is used to document APIs that use - WebSockets, MQTT, and other messaging protocols. - - Check out an example AsyncAPI spec for a chat application below: - - ```yaml maxLines={0} - asyncapi: 2.0.0 - info: - title: Chat server - version: 1.0.0 - - servers: - Production: - url: chat.com - protocol: ws - - channels: - "/application": - bindings: - ws: - query: - type: object - properties: - apiKey: - type: string - description: The API key for the client - minimum: 1 - bindingVersion: 0.1.0 - subscribe: - operationId: sendMessage - message: - $ref: '#/components/messages/SendMessage' - publish: - operationId: receiveMessage - message: - $ref: '#/components/messages/ReceiveMessage' - - components: - messages: - SendMessage: - payload: - message: string - ReceiveMessage: - payload: - message: string - from: - type: string - description: The userId for the sender of the message - ``` - - - - The Fern Definition is our take on a simpler API definition format. It is designed with **best-practices**, - supports **both RESTful and event-driven APIs**, and is optimized for **SDK generation**. - - - The Fern Definition is inspired from internal API Definition formats built at companies like - [Amazon](https://smithy.io/2.0/index.html), [Google](https://grpc.io/), [Palantir](https://blog.palantir.com/introducing-conjure-palantirs-toolchain-for-http-json-apis-2175ec172d32), - Twilio and Stripe. These companies **rejected** OpenAPI and built their own version. - - - Check out an example Fern Definition below: - - ```yaml maxLines={0} - types: - MovieId: string - - Movie: - properties: - id: MovieId - title: string - rating: - type: double - docs: The rating scale is one to five stars - - CreateMovieRequest: - properties: - title: string - rating: double - - service: - auth: false - base-path: /movies - endpoints: - createMovie: - docs: Add a movie to the database - method: POST - path: /create-movie - request: CreateMovieRequest - response: MovieId - - getMovie: - method: GET - path: /{movieId} - path-parameters: - movieId: MovieId - response: Movie - errors: - - MovieDoesNotExistError - - errors: - MovieDoesNotExistError: - status-code: 404 - type: MovieId - ``` - - - - [OpenRPC](https://open-rpc.org/) is a spec for describing JSON-RPC 2.0 APIs. It enables interactive docs, code generation, and tooling—bringing OpenAPI-style benefits to the JSON-RPC ecosystem. - - Check out an example OpenRPC Specification for a crypto wallet service below: - - ```json maxLines={0} - { - "openrpc": "1.2.6", - "info": { - "title": "Crypto Wallet Service", - "version": "1.0.0", - "description": "A simple JSON-RPC API for managing a crypto wallet." - }, - "methods": [ - { - "name": "getBalance", - "summary": "Get the balance of a wallet address.", - "params": [ - { - "name": "address", - "schema": { "type": "string" }, - "description": "The wallet address." - } - ], - "result": { - "name": "balance", - "schema": { "type": "number" }, - "description": "The balance in the wallet." - } - }, - { - "name": "sendTransaction", - "summary": "Send crypto to another address.", - "params": [ - { "name": "from", "schema": { "type": "string" }, "description": "Sender address." }, - { "name": "to", "schema": { "type": "string" }, "description": "Recipient address." }, - { "name": "amount", "schema": { "type": "number" }, "description": "Amount to send." } - ], - "result": { - "name": "txId", - "schema": { "type": "string" }, - "description": "Transaction ID." - } - } - ] - } - ``` - - - -## Why create an API Definition ? - -Once you have an API definition, Fern will use it as an input to generate artifacts -like SDKs and API Reference documentation. Every time you update the API definition, -you can regenerate these artifacts and ensure they are always up-to-date. - - - - Client libraries in multiple languages. - - - A Stripe-like API documentation website. - - } - > - A published Postman collection, with example request and responses. - - } - > - Pydantic models for FastAPI or controllers for your Spring Boot application. - - diff --git a/fern/products/api-definition/pages/introduction/what-is-the-fern-folder.mdx b/fern/products/api-definition/pages/introduction/what-is-the-fern-folder.mdx deleted file mode 100644 index 7f06ece3e..000000000 --- a/fern/products/api-definition/pages/introduction/what-is-the-fern-folder.mdx +++ /dev/null @@ -1,127 +0,0 @@ ---- -title: The Fern Folder -description: Describes the fern folder structure and its contents ---- - -Configuring fern starts with the `fern` folder which contains your API definitions, generators, and your CLI version. - -## Directory structure - -When you run `fern init`, your fern folder will be initialized with the following files: -```bash -fern/ - ├─ fern.config.json - ├─ generators.yml - └─ definition/ - ├─ api.yml - └─ imdb.yml -``` - -If you want to initialize Fern with an OpenAPI Specification, run `fern init --openapi path/to/openapi` instead. -```yaml -fern/ - ├─ fern.config.json - ├─ generators.yml # required on Fern version 0.41.0 and above - └─ openapi/ - ├─ openapi.yml -``` - -### `fern.config.json` - -Every fern folder has a single `fern.config.json` file. This file stores the organization and -the version of the Fern CLI that you are using. - -```json -{ - "organization": "imdb", - "version": "" -} -``` - -Every time you run a fern CLI command, the CLI downloads itself at the correct version to ensure -determinism. - -To upgrade the CLI, run `fern upgrade`. This will update the version field in `fern.config.json` - -### `generators.yml` - -The `generators.yml` file can include information about where your API specification is located, along with which generators you are using, where each package gets published, as well as configuration specific to each generator. - - - -```yaml -api: - path: ./path/to/openapi.yml -groups: - public: - generators: - - name: fernapi/fern-python-sdk - version: 3.0.0 - output: - location: pypi - package-name: imdb - token: ${PYPI_TOKEN} - github: - repository: imdb/imdb-python - config: - client_class_name: imdb - - name: fernapi/fern-typescript-node-sdk - version: 0.31.0 - output: - location: npm - package-name: imdb - token: ${NPM_TOKEN} - github: - repository: imdb/imdb-node - config: - namespaceExport: imdb -``` - - -```yaml -api: - path: ./path/to/openapi.yml -``` - - - -## Multiple APIs - -The fern folder is capable of housing multiple API definitions. Instead of placing your API definition at the top-level, you can nest them within an `apis` folder. Be sure to include a `generators.yml` file within each API folder that specifies the location of the API definition. - - - -```bash -fern/ - ├─ fern.config.json - ├─ generators.yml - └─ apis/ - └─ imdb/ - ├─ generators.yml - └─ openapi/ - ├─ openapi.yml - └─ disney/ - ├─ generators.yml - └─ openapi/ - ├─ openapi.yml -``` - - -```bash -fern/ - ├─ fern.config.json - ├─ generators.yml - └─ apis/ - └─ imdb/ - ├─ generators.yml - └─ definition/ - ├─ api.yml - └─ imdb.yml - └─ disney/ - ├─ generators.yml - └─ definition/ - ├─ api.yml - └─ disney.yml -``` - - diff --git a/fern/products/api-definition/pages/openapi/auth.mdx b/fern/products/api-definition/pages/openapi/auth.mdx deleted file mode 100644 index 1f9ea850b..000000000 --- a/fern/products/api-definition/pages/openapi/auth.mdx +++ /dev/null @@ -1,196 +0,0 @@ ---- -title: Authentication -subtitle: Model auth schemes such as bearer, basic, and api key. ---- - -Configuring authentication schemes happens in the `components.securitySchemes` section of OpenAPI. - -```yml title="openapi.yml" {2-3} -components: - securitySchemes: - ... -``` - - -To apply a security scheme across all endpoints, reference the `securityScheme` within the `security` section of your OpenAPI Specification. - -```yml title="openapi.yml" {3, 5-6} -components: - securitySchemes: - AuthScheme: - ... -security: - - AuthScheme: [] -``` - - -## Bearer security scheme - -Start by defining a `bearer` security scheme in your `openapi.yml`: - -```yml title="openapi.yml" {3-5} -components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer -``` - -This will generate an SDK where the user would have to provide -a mandatory argument called `token`. - -```ts index.ts -const client = new Client({ - token: "ey34..." -}) -``` - -If you want to control variable naming and the environment variable to scan, -use the configuration below: - -```yaml title="openapi.yml" {6-8} -components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer - x-fern-bearer: - name: apiKey - env: PLANTSTORE_API_KEY -``` - -The generated SDK would look like: - -```ts index.ts - -// Uses process.env.PLANTSTORE_API_KEY -let client = new Client(); - -// token has been renamed to apiKey -client = new Client({ - apiKey: "ey34..." -}) -``` - -## Basic security scheme - -Start by defining a `basic` security scheme in your `openapi.yml`: - -```yaml title="openapi.yml" {3-5} -components: - securitySchemes: - BasicAuth: - type: http - scheme: basic -``` - -This will generate an SDK where the user would have to provide -a mandatory arguments called `username` and `password`. - -```ts index.ts -const client = new Client({ - username: "joeschmoe" - password: "ey34..." -}) -``` - -If you want to control variable naming and environment variables to scan, -use the configuration below: - -```yaml title="openapi.yml" {6-12} -components: - securitySchemes: - BasicAuth: - type: http - scheme: basic - x-fern-basic: - username: - name: clientId - env: PLANTSTORE_CLIENT_ID - password: - name: clientSecret - env: PLANTSTORE_CLIENT_SECRET -``` - -The generated SDK would look like: - -```ts index.ts - -// Uses process.env.PLANTSTORE_CLIENT_ID and process.env.PLANTSTORE_CLIENT_SECRET -let client = new Client(); - -// parameters have been renamed -client = new Client({ - clientId: "joeschmoe", - clientSecret: "ey34..." -}) -``` - -## ApiKey security scheme - -Start by defining an `apiKey` security scheme in your `openapi.yml`: - -```yml title="openapi.yml" {3-5} -components: - securitySchemes: - ApiKey: - type: apiKey - in: header - name: X_API_KEY -``` - -This will generate an SDK where the user would have to provide -a mandatory argument called `apiKey`. - -```ts index.ts -const client = new Client({ - apiKey: "ey34..." -}) -``` - -If you want to control variable naming and environment variables to scan, -use the configuration below: - -```yaml title="openapi.yml" {7-10} -components: - securitySchemes: - ApiKey: - type: apiKey - in: header - name: X_API_KEY - x-fern-header: - name: apiToken - env: PLANTSTORE_API_KEY - prefix: "Token " # Optional -``` - -The generated SDK would look like: - -```ts index.ts - -// Uses process.env.PLANTSTORE_API_KEY -let client = new Client(); - -// parameters have been renamed -client = new Client({ - apiToken: "ey34..." -}) -``` - -## Multiple security schemes - -If you would like to define multiple security schemes, simply -list them under `components.securitySchemes`. For example, if you wanted to support -`basic` and `apiKey` security schemes, see the example below: - -```yaml title="openapi.yml" {3,6} -components: - securitySchemes: - BearerAuth: - type: http - scheme: bearer - ApiKey: - type: apiKey - in: header - name: X_API_KEY -``` \ No newline at end of file diff --git a/fern/products/api-definition/pages/openapi/automation.mdx b/fern/products/api-definition/pages/openapi/automation.mdx deleted file mode 100644 index 042f39552..000000000 --- a/fern/products/api-definition/pages/openapi/automation.mdx +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Sync your OpenAPI Specification -subtitle: Pull your latest OpenAPI Specification into your fern folder automatically. ---- - -If you host your OpenAPI Specification at a publicly available URL, you can have Fern programmatically fetch the latest spec on a preconfigured cadence through the [sync-openapi GitHub Action](https://github.com/fern-api/sync-openapi). This ensures your committed OpenAPI spec stays up to date with your live API. -## Setup - - - Add the origin field to your generators.yml to specify where your OpenAPI spec is hosted: - ```yml title="generators.yml" - api: - path: openapi/openapi.json - origin: https://api.example.com/openapi.json - ``` - - - Create `.github/workflows/sync-openapi.yml` in your repository: - ```yml - name: Sync OpenAPI Specs # can be customized - on: # additional custom triggers can be configured - workflow_dispatch: # manual dispatch - push: - branches: - - main # on push to main - schedule: - - cron: '0 3 * * *' # everyday at 3:00 AM UTC - jobs: - update-from-source: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - token: ${{ secrets.OPENAPI_SYNC_TOKEN }} - - name: Update API with Fern - uses: fern-api/sync-openapi@v2 - with: - update_from_source: true - token: ${{ secrets.OPENAPI_SYNC_TOKEN }} - branch: 'update-api' - auto_merge: false - add_timestamp: true -``` - - - Generate a [fine-grained personal access token](https://github.com/settings/personal-access-tokens) with read/write access to your repository. - - - Navigate to your repository's `Settings > Secrets and variables > Actions`. Select **New repository secret**, name it `OPENAPI_SYNC_TOKEN`, add your token, and click **Add secret**. - - -By default, this will create daily PRs with API spec updates to the repo containing your fern folder. If you would like to adjust the frequency, learn more about GitHub's [schedule event](https://docs.github.com/en/actions/reference/events-that-trigger-workflows#schedule). - - For detailed configuration options and other use cases, see the [sync-openapi GitHub Action README](https://github.com/fern-api/sync-openapi). diff --git a/fern/products/api-definition/pages/openapi/endpoints/multipart.mdx b/fern/products/api-definition/pages/openapi/endpoints/multipart.mdx deleted file mode 100644 index 800fa0fa1..000000000 --- a/fern/products/api-definition/pages/openapi/endpoints/multipart.mdx +++ /dev/null @@ -1,75 +0,0 @@ ---- -title: Multipart File Upload -subtitle: Document endpoints with the `multipart/form-data` content type ---- - -Multipart requests combine one or more sets of data into a single body, separated by boundaries. -You typically use these requests for file uploads and for transferring data of several types in a single request -(for example, a file along with a JSON object). - -```yml title="openapi.yml" maxLines=0 {12-24} -paths: - /upload: - post: - summary: Upload a file - description: Upload a file using multipart/form-data encoding - operationId: uploadFile - tags: - - file - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - properties: - file: - type: string - format: binary - description: The file to upload - description: - type: string - description: A description of the file (optional) - required: - - file - responses: - "200": - description: Successful upload - content: - application/json: - schema: - type: object - properties: - message: - type: string - fileId: - type: string -``` -Any request body that is defined with a `multipart/form-data` content type, will be -treated as a multipart request. Within a given multipart request, a string parameter with -`format:binary` will represent an arbitrary file. - -## Array of Files - -If your endpoint supports an array of files, then your request body must use -an array type. - -```yml openapi.yml {12-17} -paths: - /upload: - post: - summary: Upload multiple files - operationId: uploadFiles - requestBody: - content: - multipart/form-data: - schema: - type: object - properties: - files: - type: array - items: - type: string - format: binary - description: An array of files to upload -``` diff --git a/fern/products/api-definition/pages/openapi/endpoints/rest.mdx b/fern/products/api-definition/pages/openapi/endpoints/rest.mdx deleted file mode 100644 index a4dd8fcb4..000000000 --- a/fern/products/api-definition/pages/openapi/endpoints/rest.mdx +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: HTTP JSON Endpoints -subtitle: Document HTTP JSON APIs with the `application/json` content type ---- - -Endpoints in OpenAPI are defined underneath the `paths` key. Below is an example of defining -a single endpoint: - -```yml title="openapi.yml" maxLines=0 {2-18} -paths: - /pets: - post: - summary: Create a new pet - description: Creates a new pet with the provided information - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - responses: - '200': - description: User created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' -``` - -## Examples - -You can provide examples of requests and responses by using the `examples` key. - -```yaml title="openapi.yml" {12-17,25-30} -paths: - /pets: - post: - summary: Create a new pet - description: Creates a new pet with the provided information - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - examples: - PetExample: - summary: This is an example of a Pet - value: - name: Markley - id: 44 - responses: - '200': - description: A Pet object - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - examples: - PetExample: - summary: This is an example of a Pet - value: - name: Markley - id: 44 -``` - diff --git a/fern/products/api-definition/pages/openapi/endpoints/sse.mdx b/fern/products/api-definition/pages/openapi/endpoints/sse.mdx deleted file mode 100644 index bc680cfe1..000000000 --- a/fern/products/api-definition/pages/openapi/endpoints/sse.mdx +++ /dev/null @@ -1,109 +0,0 @@ ---- -title: Server-Sent Events and Streaming APIs -subtitle: Use the `x-fern-streaming` extension to model streaming endpoints ---- - - - The `x-fern-streaming` extension allows you to represent endpoints that are streaming. - - - -## JSON streaming - -If your API returns a series of `JSON` chunks as seen below - -```json -{ "text": "Hi, I am a" } -{ "text": "chatbot. Do you have any"} -{ "text": "questions for me"} -``` - -then simply add the `x-fern-streaming: true` to your OpenAPI operation. - -```yaml title="openapi.yml" {4} -paths: - /logs: - post: - x-fern-streaming: true - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/Chat" -components: - schemas: - Chat: - type: object - properties: - text: - type: string -``` - -## Server-sent events - -If your API returns server-sent-events, with the `data` and `event` keys as seen below - -```json -data: { "text": "Hi, I am a" } -data: { "text": "chatbot. Do you have any"} -data: { "text": "questions for me"} -``` - -then make sure to include `format: sse`. - -```yaml title="openapi.yml" {4-5} -paths: - /logs: - post: - x-fern-streaming: - format: sse - responses: - "200": - content: - application/json: - schema: - $ref: "#/components/schemas/Chat" -components: - schemas: - Chat: - type: object - properties: - text: - type: string -``` - -## `Stream` parameter - -It has become common practice for endpoints to have a `stream` parameter that -controls whether the response is streamed or not. Fern supports this pattern in a first -class way. - -Simply specify the `stream-condition` as well as the ordinary response and the streaming response: - -```yaml title="openapi.yml" {4-10} -paths: - /logs: - post: - x-fern-streaming: - format: sse - stream-condition: $request.stream - response: - $ref: '#/components/schemas/Chat' - response-stream: - $ref: '#/components/schemas/ChatChunk' -components: - schemas: - Chat: - type: object - properties: - text: - type: string - tokens: - type: number - ChatChunk: - type: object - properties: - text: - type: string -``` \ No newline at end of file diff --git a/fern/products/api-definition/pages/openapi/examples.mdx b/fern/products/api-definition/pages/openapi/examples.mdx deleted file mode 100644 index e4a77240e..000000000 --- a/fern/products/api-definition/pages/openapi/examples.mdx +++ /dev/null @@ -1,93 +0,0 @@ ---- -title: How to use examples in OpenAPI -description: Use the examples feature of OpenAPI to add example values in your API definition. Fern then uses your examples when generating SDKs and documentation. ---- - -Using examples in OpenAPI shows API consumers what requests and responses look like. They can be provided for request bodies, response bodies, and individual parameters. - -## Inline examples - -Examples can be placed directly within the operation definition under `paths`. Here's an example: - -```yaml -paths: - /pet: - post: - summary: Add a new pet to the store - operationId: addPet - responses: - '200': - description: A Pet object - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - examples: - PetExample: - summary: This is an example of a Pet - value: - name: Markley - id: 44 -``` - -## Reusable examples - -For more general examples that apply to multiple parts of the API, you can define them under the `components/examples` section. These can be referenced elsewhere in the documentation. - -```yaml -components: - examples: - PetExample: - summary: Example of a Pet object - value: - name: Markley - id: 44 - -paths: - /pet: - post: - summary: Add a new pet to the store - operationId: addPet - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Pet' - examples: - PetExample: - $ref: '#/components/examples/PetExample' -``` - -## How examples are used in Fern SDKs - -Fern SDKs use examples from your OpenAPI document to generate comments that show up in your IDE. For example, in a Node.js SDK: - - -```ts - -import * as Petstore from "../../.."; - -/** - * @example - * { - * name: "Markley", - * id: "44" - * } - */ -``` - - -Here's an [example in GitHub](https://github.com/FlatFilers/flatfile-node/blob/ab955a0a337c40ea00755e24df08f8c9a146c39c/src/api/resources/documents/types/DocumentResponse.ts#L8-L27) from Flatfile's Node.js SDK. - -## How examples are used in Fern Docs - -In the request and response code snippets, you'll see the example values used. - - -![Screenshot of an example used in response code in Fern Docs API reference](https://fern-image-hosting.s3.amazonaws.com/movie+example.png) - - -If you generate SDKs with Fern, the code examples for each language will also be populated with the example values. [Check out Flatfile's Docs to see this in action](https://reference.flatfile.com/api-reference/documents/create). Change the language toggle to see the examples in different languages. - diff --git a/fern/products/api-definition/pages/openapi/extensions/audiences.mdx b/fern/products/api-definition/pages/openapi/extensions/audiences.mdx deleted file mode 100644 index 8bd80fc5f..000000000 --- a/fern/products/api-definition/pages/openapi/extensions/audiences.mdx +++ /dev/null @@ -1,118 +0,0 @@ ---- -title: Use audiences to filter your API -subtitle: Use `x-fern-audiences` to filter to relevant endpoints, schemas and properties ---- - -Audiences are a useful tool for segmenting your API for different consumers. Common examples of audiences include `public` -and `beta`. - - - Remember to filter your SDKs and Docs after specifying audiences. If **no audiences** are specified, - nothing will be filtered. - - - - -The following example configures the SDK to filter to the `public` audience: - -```yaml title="generators.yml" {3-4} -groups: - sdks: - audiences: - - public - generators: - - name: fernapi/fern-typescript-node-sdk - version: 0.8.8 -``` - - - -The following example configures the docs to filter to the `public` audience: - -```yaml title="docs.yml" {3-4} -navigation: - - api: API Reference - audiences: - - public -``` - - - - - - -## Audiences for servers - -To mark a server with a particular audience, add the `x-fern-server-name` and `x-fern-audiences` extension to the relevant server. - -In the example below, the `Production` server is only available to public consumers: - -```yaml title="openapi.yml" {3-5} -servers: - - url: https://api.com - x-fern-server-name: Production - x-fern-audiences: - - public -``` - -## Audiences for endpoints - -To mark an endpoint with a particular audience, add the `x-fern-audiences` extension to the relevant endpoint. - -In the example below, the `POST /users/sendEmail` endpoint is only available to public consumers: - -```yaml title="openapi.yml" {4-5} -paths: - /users/sendEmail: - post: - x-fern-audiences: - - public - operationId: send_email -``` - -## Audiences for schemas - -Schemas can be marked for different audiences, as well. - -In this example, the `Email` type is available to both public and beta customers. - -```yaml title="openapi.yml" {13-15} -components: - schemas: - Email: - title: Email - type: object - properties: - subject: - type: string - body: - type: string - to: - type: string - x-fern-audiences: - - public - - beta -``` - -## Audiences for properties - -Properties can be marked for different audiences, as well. - -In this example, the `to` property is available to beta customers only. - -```yaml title="openapi.yml" {13-17} -components: - schemas: - Email: - title: Email - type: object - properties: - subject: - type: string - body: - type: string - to: - type: string - x-fern-audiences: - - beta -``` diff --git a/fern/products/api-definition/pages/openapi/extensions/method-names.mdx b/fern/products/api-definition/pages/openapi/extensions/method-names.mdx deleted file mode 100644 index 6a5c42306..000000000 --- a/fern/products/api-definition/pages/openapi/extensions/method-names.mdx +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Customize SDK Method Names -description: Use `x-fern-sdk-method-name` and `x-fern-sdk-group-name` to finetune SDK naming. ---- - -## Operation IDs - -By default, if you have no extensions present, Fern will try to use your operation ID to generate idiomatic -method names for the SDK. We typically recommend formatting your operation IDs like `{tag_name}_{operation_name}`. - -For example, for an endpoint that has the tag `users` and the operation id `users_get`, we will generate an SDK -method that is `users.get()`. If your operation id does not start with a tag, then we will simply use it as the method name. - -## Usage - - - The `x-fern-sdk-group-name` and `x-fern-sdk-method-name` extensions allow you to customize the generated SDK method - names. - - -In the example below, Fern will generate a method called `client.users.create()` for the `POST /users` endpoint. - -```yaml title="openapi.yaml" -paths: - /users: - post: - x-fern-sdk-group-name: users - x-fern-sdk-method-name: create -``` - -## Top level methods - -If you omit the `x-fern-sdk-group-name` extension, then the generated SDK method will live at the root. -In the example below, Fern will generate a method called `client.send()`: - -```yaml title="openapi.yaml" -paths: - /send: - post: - x-fern-sdk-method-name: send -``` - -## Multiple levels of nesting - -If you add more than one `x-fern-sdk-group-name` extension, then the generated SDK will nest group names. -The order of the group names is preserved in the generated SDK method. - -In the example below, Fern will generate a method called `client.users.notifications.send()`: - -```yaml title="openapi.yaml" -paths: - /users/notifications: - post: - x-fern-sdk-group-name: - - users - - notifications - x-fern-sdk-method-name: send -``` diff --git a/fern/products/api-definition/pages/openapi/extensions/others.mdx b/fern/products/api-definition/pages/openapi/extensions/others.mdx deleted file mode 100644 index b3aa4b3e3..000000000 --- a/fern/products/api-definition/pages/openapi/extensions/others.mdx +++ /dev/null @@ -1,416 +0,0 @@ ---- -title: Other extensions -description: Learn about Fern's OpenAPI extensions for authentication overrides, global headers, enum descriptions and names, audiences, and more. ---- - -Fern supports different OpenAPI extensions so that you can generate higher-quality SDKs. - -## API version - -You can define your API version scheme, such as a `X-API-Version` header. The supported versions and default value are specified like so: - -```yaml title="openapi.yaml" -x-fern-version: - version: - header: X-API-Version - default: "2.0.0" - values: - - "1.0.0" - - "2.0.0" - - "latest" -paths: ... -``` - -## Global headers - -At times, your API will leverage certain headers for every endpoint, or the majority of them, we call these "global headers". For convenience, generated Fern SDKs expose "global headers" to easily be updated on API calls. Take for example an API key, if we declare the API key as a global header, a user will be able to plug theirs in easily: - -```python -import os - -class Client: - - def __init__(self, *, apiKey: str): -``` - -To configure global headers, Fern will automatically pull out headers that are used in every request, or the majority of requests, and mark them as global. -In order to label additional headers as global, or to alias the names of global headers, you can leverage the `x-fern-global-headers` extension: - -```yaml title="openapi.yml" -x-fern-global-headers: - - header: custom_api_key - name: api_key - - header: userpool_id - optional: true -``` - -yields the following client: - -```python -import os - -class Client: - - def __init__(self, *, apiKey: str, userpoolId: typing.Optional[str]) -``` - -## Enum descriptions and names - -OpenAPI doesn't natively support adding descriptions to enum values. To do this in Fern you can use the `x-fern-enum` -extension. - -In the example below, we've added some descriptions to enum values. These descriptions will -propagate into the generated SDK and docs website. - -```yaml title="openapi.yml" {9-13} -components: - schemas: - CardSuit: - enum: - - clubs - - diamonds - - hearts - - spades - x-fern-enum: - clubs: - description: Some docs about clubs - spades: - description: Some docs about spades -``` - -`x-fern-enum` also supports a `name` field that allows you to customize the name of the enum in code. -This is particularly useful when you have enums that rely on symbolic characters that would otherwise cause -generated code not to compile. - -For example, the following OpenAPI - -```yaml title="openapi.yml" {9,12} -components: - schemas: - Operand: - enum: - - '>' - - '<' - x-fern-enum: - '>': - name: GreaterThan - description: Checks if value is greater than - '<': - name: LessThan - description: Checks if value is less than -``` - -would generate - -```typescript title="operand.ts" -export enum Operand { - GreaterThan = ">", - LessThan = "<" -} -``` - -## Schema names - -OpenAPI allows you to define inlined schemas that do not have names. - -```yaml title="Inline type in openapi.yml" {11} -components: - schemas: - Movie: - type: object - properties: - name: - type: string - cast: - type: array - items: - type: object - properties: - firstName: - type: string - lastName: - type: string - age: - type: integer -``` - -Fern automatically generates names for all the inlined schemas. For example, in this example, -Fern would generate the name `CastItem` for the inlined array item schema. - -```typescript title="Auto-generated name" {6} -export interface Movie { - name?: string; - cast?: CastItem[]; -} - -export interface CastItem { - firstName?: string; - lastName?: string; - age?: integer; -} -``` - -If you want to override the generated name, you can use the extension `x-fern-type-name`. - -```yaml title="openapi.yml" {12} -components: - schemas: - Movie: - type: object - properties: - name: - type: string - cast: - type: array - items: - type: object - x-fern-type-name: Person - properties: - firstName: - type: string - lastName: - type: string - age: - type: integer -``` - -This would replace `CastItem` with `Person` and the generated code would read more idiomatically: - -```typescript title="Overridden name" {6} -export interface Movie { - name?: string; - cast?: Person[]; -} - -export interface Person { - firstName?: string; - lastName?: string; - age?: integer; -} -``` - -## Property names - -The `x-fern-property-name` extension allows you to customize the variable name for object -properties. - -For example, if you had a property called `_metadata` in your schema but you wanted the -variable to be called `data` in your SDK you would do the following: - -```yaml {6} -components: - schemas: - MyUser: - _metadata: - type: object - x-fern-property-name: data -``` - -## Server names - -The `x-fern-server-name` extension is used to name your servers. - -```yaml title="openapi.yml" -servers: - - url: https://api.example.com - x-fern-server-name: Production - - url: https://sandbox.example.com - x-fern-server-name: Sandbox -``` - -In a generated TypeScript SDK, you'd see: - -```typescript title="environment.ts" -export const ExampleEnvironment = { - Production: "https://api.example.com" -} as const; - -export type ExampleEnvironment = typeof ExampleEnvironment.Production; -``` - -## Base path - -The `x-fern-base-path` extension is used to configure the base path prepended to every endpoint. - -In the example below, we have configured the `/v1` base path so the full endpoint path is -`https://api.example.com/v1/users`. - -```yaml title="Set the base path in openapi.yml" {1} -x-fern-base-path: /v1 -servers: - - url: https://api.example.com -paths: - /users: ... -``` - -## Ignoring schemas or endpoints - -If you want Fern to skip reading any endpoints or schemas, use the `x-fern-ignore` extension. - -To skip an endpoint, add `x-fern-ignore: true` at the operation level. - -```yaml title="x-fern-ignore at operation level in openapi.yml" {4} -paths: - /users: - get: - x-fern-ignore: true - ... -``` - -To skip a schema, add `x-fern-ignore: true` at the schema level. - -```yaml title="x-fern-ignore at schema level in openapi.yml" {4} -components: - schemas: - SchemaToSkip: - x-fern-ignore: true - ... -``` - -## Overlaying extensions - -Because of the number of tools that use OpenAPI, it may be more convenient to -"overlay" your fern specific OpenAPI extensions onto your original definition. \ -In order to do this you can specify your overrides file in `generators.yml`. - -Below is an example of how someone can overlay the extensions `x-fern-sdk-method-name` and -`x-fern-sdk-group-name` without polluting their original OpenAPI. The combined result is -shown in the third tab. - - - ```yaml title="generators.yml" {3} - api: - path: ./openapi/openapi.yaml - overrides: ./openapi/overrides.yaml - default-group: sdk - groups: - sdk: - generators: - - name: fernapi/fern-python-sdk - version: 2.2.0 - ``` - - ```yaml title="overrides.yml" - paths: - /users: - get: - x-fern-sdk-group-name: users - x-fern-sdk-method-name: get - ``` - - ```yaml title="Overlaid OpenAPI" {4-5} - paths: - /users: - get: - x-fern-sdk-group-name: users - x-fern-sdk-method-name: get - summary: Get a list of users - description: Retrieve a list of users from the system. - responses: - '200': - description: Successful response - '500': - description: Internal Server Error - ``` - - - -## Embedding extensions - -If instead of overlaying your extensions within an overrides file, as mentioned above. Certain frameworks that generate OpenAPI Specifications make it easy to embed extensions directly from code. - -### FastAPI - -Please view our page on [FastAPI](/learn/api-definition/openapi/frameworks/fastapi) for more information on how to extend your OpenAPI Specification within FastAPI. - -## Request + response examples - -While OpenAPI has several fields for examples, there is no easy way -to associate a request with a response. This is especially useful when -you want to show more than one example in your documentation. - -`x-fern-examples` is an array of examples. Each element of the array -can contain `path-parameters`, `query-parameters`, `request` and `response` -examples values that are all associated. - -```yaml title="openapi.yml" {5-16} -paths: - /users/{userId}: - get: - x-fern-examples: - - path-parameters: - userId: user-1234 - response: - body: - name: Foo - ssn: 1234 - - path-parameters: - userId: user-4567 - response: - body: - name: Foo - ssn: 4567 -components: - schemas: - User: - type: object - properties: - name: - type: string - ssn: - type: integer -``` - -### Code samples - -If you'd like to specify custom code samples for your example, use `code-samples`. - -```yaml title="openapi.yml" {11-16} -paths: - /users/{userId}: - get: - x-fern-examples: - - path-parameters: - userId: user-1234 - response: - body: - name: Foo - ssn: 1234 - code-samples: - - sdk: typescript - code: | - import { UserClient } from "..."; - - client.users.get("user-1234") -``` - -If you're on the Fern Basic plan or higher for SDKs you won't have to worry about manually adding code samples! Our generators do that for you. - -## Availability - -The `x-fern-availability` extension is used to mark the availability of an endpoint. The availability information propagates into the generated Fern Docs website as visual tags. - -The options are: - -- `beta` -- `generally-available` -- `deprecated` - -The example below marks that the `POST /pet` endpoint is `deprecated`. - -```yaml title="x-fern-availability in openapi.yml" {4} -paths: - /pet: - post: - x-fern-availability: deprecated -``` - -This renders as: - - -![Screenshot of API Reference endpoint with tag showing deprecated](https://fern-image-hosting.s3.amazonaws.com/fern/x-fern-availability-example.png) - - -### Request new extensions - -If there's an extension you want that doesn't already exist, file an [issue](https://github.com/fern-api/fern/issues/new) to start a discussion about it. diff --git a/fern/products/api-definition/pages/openapi/extensions/parameter-names.mdx b/fern/products/api-definition/pages/openapi/extensions/parameter-names.mdx deleted file mode 100644 index ff60bf613..000000000 --- a/fern/products/api-definition/pages/openapi/extensions/parameter-names.mdx +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Customize parameter names -description: Use `x-fern-parameter-name` to customize query parameter, header and path parameter naming. ---- - - - The `x-fern-parameter-name` extension allows you to customize the variable names of parameters in your generated SDKs. - - -## Headers - -In the example below, the header `X-API-Version` is renamed to `version` in the -generated SDK. The rename makes the SDK more human readable. - -```yaml {8} -paths: - "/user": - get: - operationId: list_user - parameters: - - in: header - name: X-API-Version - x-fern-parameter-name: version - schema: - type: string - required: true -``` - -## Query parameters - -In the example below, the query parameter `q` is renamed to `search_terms` in the -generated SDK. The rename makes the parameter more approachable for end users. - -```yaml {8} -paths: - "/user/search": - get: - operationId: search_user - parameters: - - in: query - name: q - x-fern-parameter-name: search_terms - schema: - type: string - required: false -``` - -## Path parameters - -In the example below, the path parameter `userId` is renamed to `id` in the -generated SDK. The rename makes the SDK less verbose. - -```yaml {8} -paths: - "/user/{userId}": - get: - operationId: get_user - parameters: - - in: path - name: userId - x-fern-parameter-name: id - schema: - type: string - required: false -``` diff --git a/fern/products/api-definition/pages/openapi/overrides.mdx b/fern/products/api-definition/pages/openapi/overrides.mdx deleted file mode 100644 index 50d6462d7..000000000 --- a/fern/products/api-definition/pages/openapi/overrides.mdx +++ /dev/null @@ -1,74 +0,0 @@ ---- -title: Overlay customizations on an existing OpenAPI spec -subtitle: Can't directly modify your OpenAPI spec? No worries, use an overrides file instead. ---- - -If you generate your OpenAPI from server code, you may want to tweak your OpenAPI Spec without having to -touch the generated file. Fern supports this via an `overrides` file. - - -```yml openapi.yml -paths: - /users: - post: - description: Create a User - operationId: users_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/User' -``` -```yml title="overrides.yml" {4-5} -paths: - /users: - post: - x-fern-sdk-group-name: users - x-fern-sdk-method-name: create -``` -```yml title="combined" {4-5} -paths: - /users/post: - post: - x-fern-sdk-group-name: users - x-fern-sdk-method-name: create - description: Create a User - operationId: users_post - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/User' -``` - - -## Configuration - -Follow the steps below to configure your OpenAPI overrides: - - -### Create an `overrides.yml` - -Simply create a yaml file and write down all the overrides you want to add: - -```yaml overrides.yml -paths: - /v1/history: - get: - x-fern-sdk-group-name: - - history - x-fern-sdk-method-name: get_all -``` - -### Reference the file in your `generators.yml` - -```yml generators.yml -api: - path: ../openapi.yml - overrides: ../overrides.yml -``` - - The format of the overrides file is independent from the spec. For example, even if your OpenAPI spec is in JSON format, you can write the overrides in yaml. - - - diff --git a/fern/products/api-definition/pages/openapi/overview.mdx b/fern/products/api-definition/pages/openapi/overview.mdx deleted file mode 100644 index c6cc053e0..000000000 --- a/fern/products/api-definition/pages/openapi/overview.mdx +++ /dev/null @@ -1,128 +0,0 @@ ---- -title: What is an OpenAPI Specification? -subtitle: OpenAPI is a standard for documenting REST APIs ---- - -The OpenAPI Specification (OAS) is a framework used by developers to document REST APIs. The specification -written in JSON or YAML and contains all of your endpoints, parameters, schemas, and authentication schemes. -Fern is compatible with the latest OAS release, which is currently [v3.1.1](https://spec.openapis.org/#openapi-specification). - - Considering options to generate an OpenAPI spec? Get live support [here](https://fern-community.slack.com/join/shared_invite/zt-2dpftfmif-MuAegl8AfP_PK8s2tx350Q%EF%BB%BF#/shared-invite/email) - -Below is an example of an OpenAPI file: - -```yaml openapi.yml -openapi: 3.0.2 -info: - title: Petstore - OpenAPI 3.0 - description: |- - This is a sample Pet Store Server based on the OpenAPI 3.0 specification. -paths: - "/pet": - put: - tags: - - pet - summary: Update an existing pet - description: Update an existing pet by Id - operationId: updatePet - requestBody: - description: Update an existent pet in the store - content: - application/json: - schema: - "$ref": "#/components/schemas/Pet" - required: true - responses: - '200': - description: Successful operation - content: - application/json: - schema: - "$ref": "#/components/schemas/Pet" - '400': - description: Invalid ID supplied - '404': - description: Pet not found - '405': - description: Validation exception - security: - - api_key -components: - schemas: - Category: - type: object - properties: - id: - type: integer - format: int64 - example: 1 - name: - type: string - example: Dogs - Tag: - type: object - properties: - id: - type: integer - format: int64 - name: - type: string - Pet: - required: - - name - - photoUrls - type: object - properties: - id: - type: integer - format: int64 - example: 10 - name: - type: string - example: doggie - category: - "$ref": "#/components/schemas/Category" - photoUrls: - type: array - items: - type: string - tags: - type: array - items: - "$ref": "#/components/schemas/Tag" - status: - type: string - description: pet status in the store - enum: - - available - - pending - - sold - securitySchemes: - api_key: - type: apiKey - name: api_key - in: header -``` - -## Setup your fern folder - -Start by initializing your fern folder with an OpenAPI spec - - -```sh file -fern init --openapi ./path/to/openapi -``` -```sh url -fern init --openapi https://host/path/to/openapi -``` - - -This will initialize a directory like the following -``` -fern/ - ├─ fern.config.json - ├─ generators.yml - └─ openapi/ - ├─ openapi.yml -``` - diff --git a/fern/products/api-definition/pages/openapi/server-frameworks/fastapi.mdx b/fern/products/api-definition/pages/openapi/server-frameworks/fastapi.mdx deleted file mode 100644 index df5bcbe3a..000000000 --- a/fern/products/api-definition/pages/openapi/server-frameworks/fastapi.mdx +++ /dev/null @@ -1,92 +0,0 @@ ---- -title: FastAPI Instrumentation -description: Learn about best practices for creating rich OpenAPI Specifications when instrumenting FastAPI applications. ---- - -[FastAPI](https://fastapi.tiangolo.com/) is a popular Python web framework developed by [tiangolo](https://github.com/tiangolo). - -The offering brands itself as - -> FastAPI is a modern, fast (high-performance), web framework for building APIs with Python based on standard Python type hints. - -FastAPI plays very nicely with Fern because it has the power to output OpenAPI Specifications! Below we'll outline some tips for generating a rich OpenAPI with FastAPI. - - -## OpenAPI generation - -By default, FastAPI will generate an OpenAPI Specification for you based on your routes and your data models! You can access this spec by visiting `/docs` on your FastAPI server. - -If you are not seeing any OpenAPI Specification (or the Swagger UI), you may need to review your FastAPI server configuration as the path may have been changed, or completely omitted. - -```python {6-8} -from fastapi import FastAPI - -... - -FastAPI( - openapi_url="/openapi.json", # <-- this is the file and URL needed to access the OpenAPI Specification, `docs_url` and `redoc_url` are convenient wrappers that display the file in a UI! - docs_url="/docs", # <-- this is the URL to access the Swagger UI, which will point to your OpenAPI Specification - redoc_url="/redoc" # <-- this is the URL to access the ReDoc UI, which will point to your OpenAPI Specification -) -``` - -## Specifying servers - -Fern will automatically generate clients that point to the servers you configure within your OpenAPI Specification, so it's important to specify the servers that your API will be hosted on. - -```python {5} -from fastapi import FastAPI - -... - -app = FastAPI(servers=[{"url": "http://prod.test.com", "description": "Production server"}]) -# This creates the following server object in your OpenAPI Specification: -# "servers":[{"url":"http://prod.test.com","description":"Production server"}], -``` - -## OpenAPI extensions - -FastAPI allows you to add in extra OpenAPI configuration directly within your route, through the use of the `openapi_extra` parameter. -Below, we've annotated a "good" route within FastAPI that has it's typings as well as Fern extensions to assist in naming. - -```python {5-9} -@router.post( - "/your/post/endpoint", - response_model=YourResponseModel, # <-- with FastAPI, it is important to specify your response model so that it comes through to the OpenAPI Specification - summary="Get some response for your req", # <-- if you'd like to add a description to your endpoint, you can do so here - openapi_extra={ # <-- finally, you can add in your Fern extensions here, these extensions will produce SDK code that looks something like: `client.endpoints.create(...)` in python - "x-fern-sdk-method-name": "create", - "x-fern-sdk-group-name": "endpoints", - "x-fern-availability": "beta", - }, -) -async def your_post_endpoint( - payload: YourRequestModel, -) -> YourResponseModel: -``` - -## Specifying examples - -FastAPI allows you to specify examples for your data models, which Fern will pick up and use within your generated SDKs and documentation automatically. - -For more information on leveraging examples within Fern, please refer to the [Fern documentation](/learn/api-definition/openapi/extensions/others#request--response-examples). - -For more information on this FastAPI functionality, please refer to the [FastAPI documentation](https://fastapi.tiangolo.com/tutorial/schema-extra-example/). - -```python {7-11} -from pydantic import BaseModel - -class MyObject(BaseModel): - id: str - - class Config: - schema_extra = { - "example": { - "id": "a-cool-uuid", - } - } -``` - -## Additional customization - -FastAPI has a lot of flexibility in how you can customize your OpenAPI Specification. Please refer to the [FastAPI documentation](https://fastapi.tiangolo.com/how-to/extending-openapi/#modify-the-openapi-schema) for more information. diff --git a/fern/products/api-definition/pages/openapi/servers.mdx b/fern/products/api-definition/pages/openapi/servers.mdx deleted file mode 100644 index 28ce155a2..000000000 --- a/fern/products/api-definition/pages/openapi/servers.mdx +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: Servers -description: Configure server URLs and environments to help users connect to your API. -subtitle: Define server URLs and environments to help users connect to your API. ---- - -OpenAPI allows you to specify one or more base URLs under the `servers` key. - -```yml openapi.yml - -servers: - - url: https://api.yourcompany.com/ - - url: https://api.eu.yourcompany.com/ -``` - -Specifying servers is valuable for both SDKs and Docs: -- For SDKs, your users won't need to manually specify the baseURL at client instantiation -- For Docs, your API playground will automatically hit the correct server - -## Naming your servers - -If you have more than one server, we recommend specifying an `x-fern-server-name` to name -the server. - -```yml openapi.yml {3,5} -servers: - - x-fern-server-name: Production - url: https://api.yourcompany.com/ - - x-fern-server-name: Production_EU - url: https://api.eu.yourcompany.com/ -``` - -## Multiple Base URLs for a single API - -If you have a microservice architecture, it is possible that you may have different endpoints hosted -at different URLs. For example, your AI endpoints might be hosted at `ai.yourcompany.com` and the rest -of your endpoints might be hosted at `api.yourcompany.com`. - -To specify this, you will need to add configuration to both your `generators.yml` and OpenAPI spec. The -snippet directly below shows how to configure an environment with multiple urls in your `generators.yml`. - -```yml generators.yml {3-8} -api: - default-environment: Production - default-url: api - environments: - Production: - api: api.yourcompany.com - ai: ai.yourcompany.com - specs: - - openapi: ./path/to/your/openapi - overrides: ./path/to/your/overrides # optional -``` - -Once you've specified the environments in your `generators.yml`, you can use the `x-fern-server-name` -extension to specify which server the operation belongs to. - -```yml openapi.yml {4} -paths: - /chat: - post: - x-fern-server-name: ai -``` - -If you have multiple environments like development or staging, you can model those in your `generators.yml` -as well. - -```yml generators.yml {7-12} -api: - default-environment: Production - default-url: api - environments: - Production: - api: api.yourcompany.com - ai: ai.yourcompany.com - Staging: - api: api.staging.yourcompany.com - ai: ai.staging.yourcompany.com - Dev: - api: api.dev.yourcompany.com - ai: ai.dev.yourcompany.com -``` - -To see an example of this in production, check out the Chariot [generators.yml](https://github.com/chariot-giving/chariot-openapi/blob/main/fern/apis/2025-02-24/generators.yml) \ No newline at end of file diff --git a/fern/products/api-definition/pages/openapi/webhooks.mdx b/fern/products/api-definition/pages/openapi/webhooks.mdx deleted file mode 100644 index 0bf5a2699..000000000 --- a/fern/products/api-definition/pages/openapi/webhooks.mdx +++ /dev/null @@ -1,33 +0,0 @@ ---- -title: Define Webhooks in OpenAPI -subtitle: Use the `x-fern-webhook` extension to define webhooks in your OpenAPI spec ---- - -To define a webhook in your OpenAPI specification, add the `x-fern-webhook: true` extension to your endpoint. OpenAPI 3.0.0 or higher is required. Fern will treat the `requestBody` as the webhook payload. - -```yaml openapi.yml {6} -paths: - /payment/updated/: - post: - summary: Payment Initiated - operationId: initiatePayment - x-fern-webhook: true - requestBody: - content: - application/json: - schema: - type: object - properties: - amount: - type: number - currency: - $ref: '#/components/schemas/Currency' - required: - - amount - - currency -``` - - -The path that you choose when defining a webhook can be arbitrary. Since webhooks -can be sent to any server, Fern just ignores the path. -