Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions lib/getBestPractices.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import fs from 'fs/promises'
import path from 'path'
import { fileURLToPath } from 'url'
import * as cheerio from 'cheerio'
import TurndownService from 'turndown'

const __dirname = path.dirname(fileURLToPath(import.meta.url))
const bestPracticesPath = path.join(__dirname, '..', 'best-practices.md')
const etagPath = path.join(__dirname, '..', 'best-practices.etag')

async function downloadBestPractices() {
try {
await fs.mkdir(path.dirname(bestPracticesPath), { recursive: true })
// Ensure the file exists before proceeding, creating it if necessary.
try {
await fs.access(bestPracticesPath)
} catch {
await fs.writeFile(bestPracticesPath, '')
}

const urls = {
best: 'https://cap.cloud.sap/docs/about/best-practices',
bad: 'https://cap.cloud.sap/docs/about/bad-practices'
}

let storedEtags = { best: null, bad: null }
try {
const etagContent = await fs.readFile(etagPath, 'utf-8')
const [bestEtag, badEtag] = etagContent.split('\n')
storedEtags = { best: bestEtag, bad: badEtag }
} catch {
// No stored ETag found
}

const fetchOptions = (etag) => (etag ? { headers: { 'If-None-Match': etag } } : {})

const [bestPracticeResponse, badPracticeResponse] = await Promise.all([
fetch(urls.best, fetchOptions(storedEtags.best)),
fetch(urls.bad, fetchOptions(storedEtags.bad))
])

if (bestPracticeResponse.status === 304 && badPracticeResponse.status === 304) {
// Content is unchanged for both
return
}

const turndownService = new TurndownService()

const processResponse = async (response) => {
if (response.status === 304) return { content: null, etag: response.headers.get('etag') }
if (!response.ok) throw new Error(`Failed to download from ${response.url}: ${response.status}`)
const html = await response.text()
const $ = cheerio.load(html)

// Remove unwanted sections
$('aside, nav, footer').remove()

// Only select headers and paragraphs
const filteredElements = $('h1, h2, h3, h4, h5, h6, p')
const filteredHtml = $('<div>').append(filteredElements).html()

return {
content: turndownService.turndown(filteredHtml || ''),
etag: response.headers.get('etag')
}
}

const [bestResult, badResult] = await Promise.all([
processResponse(bestPracticeResponse),
processResponse(badPracticeResponse)
])

let bestPracticesMd = bestResult.content
if (!bestPracticesMd) {
const cachedContent = await fs.readFile(bestPracticesPath, 'utf-8')
bestPracticesMd = cachedContent.split('# Things to Avoid')[0]
}

let badPracticesMd = badResult.content
if (!badPracticesMd) {
const cachedContent = await fs.readFile(bestPracticesPath, 'utf-8')
const sections = cachedContent.split('# Things to Avoid')
badPracticesMd = sections.length > 1 ? sections[1] : ''
}

const combinedMarkdown = `# Best Practices\n\n${bestPracticesMd}\n\n# Things to Avoid\n\n${badPracticesMd}`
await fs.writeFile(bestPracticesPath, combinedMarkdown)

const newBestEtag = bestResult.etag || storedEtags.best
const newBadEtag = badResult.etag || storedEtags.bad
if (newBestEtag && newBadEtag) {
await fs.writeFile(etagPath, `${newBestEtag}\n${newBadEtag}`)
}
} catch (error) {
console.error('Error downloading best practices:', error)
try {
await fs.access(bestPracticesPath)
} catch (e) {
throw new Error('Failed to fetch best practices and no cached version is available.')
}
}
}

export default async function getBestPractices({ filePath } = {}) {
// Always download and read the official best practices first.
await downloadBestPractices()
const officialContent = await fs.readFile(bestPracticesPath, 'utf-8')

// If no custom file path is provided, just return the official content.
if (!filePath) {
return officialContent
}

// If a custom file path is provided, read it and append its content.
try {
const customContent = await fs.readFile(filePath, 'utf-8')
// Combine the official and custom content with a clear separator.
return `${officialContent}\n\n---\n\n# Custom Guidelines\n\n${customContent}`
} catch (error) {
console.error(`Error reading custom guidelines file: ${error.message}`)
throw new Error(`Could not read the file at ${filePath}. Please ensure the path is correct and the file exists.`)
}
}
12 changes: 12 additions & 0 deletions lib/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,20 @@ import { z } from 'zod'
import getModel from './getModel.js'
import fuzzyTopN from './fuzzyTopN.js'
import searchMarkdownDocs from './searchMarkdownDocs.js'
import getBestPractices from './getBestPractices.js'

const tools = {
get_best_practices: {
title: 'Get CDS Best Practices to examine your code',
description:
'Returns the complete official documentation on proven best practices for CDS. Optionally, a file path to a custom markdown file can be provided, and its content will be appended to the official guidelines.',
inputSchema: {
filePath: z.string().optional().describe('Optional path to a custom markdown file for project-specific guidelines.')
},
handler: async ({ filePath }) => {
return await getBestPractices({ filePath })
}
},
search_model: {
title: 'Search for CDS definitions',
description:
Expand Down
61 changes: 61 additions & 0 deletions tests/getBestPractices.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Node.js test runner (test) for lib/getBestPractices.js
import getBestPractices from '../lib/getBestPractices.js'
import assert from 'node:assert'
import { test, mock } from 'node:test'
import fs from 'fs/promises'
import path from 'path'

test.describe('getBestPractices', () => {
test('should fetch and return best practices', async () => {
// Mock fetch to avoid actual network requests
global.fetch = mock.fn(async () => {
return {
ok: true,
status: 200,
headers: new Map([['etag', 'W/"12345"']]),
text: async () => `
<html>
<body>
<div id="some-other-content">...</div>
<h2 id="proven-best-practices">Proven Best Practices</h2>
<div>
<p>This is a best practice.</p>
<ul>
<li>Use this</li>
<li>Not that</li>
</ul>
</div>
</body>
</html>
`
}
})

// Mock fs.writeFile to prevent writing to disk
mock.method(fs, 'writeFile', async () => {})

const result = await getBestPractices()

assert(typeof result === 'string', 'Result should be a string')
assert(result.includes('This is a best practice.'), 'Result should contain best practice text')
assert(result.includes('* Use this'), 'Result should contain list items')
})

test('should use cached version on fetch failure', async () => {
// Mock fetch to simulate a failure
global.fetch = mock.fn(async () => {
return {
ok: false,
status: 500
}
})

const cachedContent = '## Proven Best Practices\n\nThis is the cached content.'
// Mock fs.readFile to return cached content
mock.method(fs, 'readFile', async () => cachedContent)

const result = await getBestPractices()

assert.equal(result, cachedContent, 'Should return cached content on failure')
})
})