feat: scraper registry + adapter interface
This commit is contained in:
19
src/lib/scrapers/index.ts
Normal file
19
src/lib/scrapers/index.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { detectShop, type Shop } from '@/lib/shops'
|
||||||
|
import type { PriceScraper, ScrapeResult } from './types'
|
||||||
|
|
||||||
|
const registry = new Map<Shop, PriceScraper>()
|
||||||
|
|
||||||
|
export function registerScraper(scraper: PriceScraper) {
|
||||||
|
registry.set(scraper.shop, scraper)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scrapeUrl(url: string): Promise<ScrapeResult> {
|
||||||
|
const shop = detectShop(url)
|
||||||
|
if (!shop) throw new Error(`Unsupported URL: ${url}`)
|
||||||
|
const scraper = registry.get(shop)
|
||||||
|
if (!scraper) throw new Error(`No scraper registered for shop: ${shop}`)
|
||||||
|
return scraper.scrape(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerScraperForTest(s: PriceScraper) { registry.set(s.shop, s) }
|
||||||
|
export function resetScrapersForTest() { registry.clear() }
|
||||||
15
src/lib/scrapers/types.ts
Normal file
15
src/lib/scrapers/types.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { Shop } from '@/lib/shops'
|
||||||
|
|
||||||
|
export interface ScrapeResult {
|
||||||
|
price: number | null
|
||||||
|
currency: string
|
||||||
|
availability: 'in_stock' | 'out_of_stock' | 'unknown'
|
||||||
|
name?: string
|
||||||
|
imageUrl?: string
|
||||||
|
error?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PriceScraper {
|
||||||
|
shop: Shop
|
||||||
|
scrape(url: string): Promise<ScrapeResult>
|
||||||
|
}
|
||||||
24
tests/scrapers/registry.test.ts
Normal file
24
tests/scrapers/registry.test.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, it, expect, vi } from 'vitest'
|
||||||
|
import { scrapeUrl, registerScraperForTest, resetScrapersForTest } from '@/lib/scrapers'
|
||||||
|
import type { PriceScraper } from '@/lib/scrapers/types'
|
||||||
|
|
||||||
|
describe('scrapeUrl', () => {
|
||||||
|
it('dispatches to matching shop scraper', async () => {
|
||||||
|
const fake: PriceScraper = {
|
||||||
|
shop: 'geizhals',
|
||||||
|
scrape: vi.fn().mockResolvedValue({
|
||||||
|
price: 42, currency: 'EUR', availability: 'in_stock', name: 'X',
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
resetScrapersForTest()
|
||||||
|
registerScraperForTest(fake)
|
||||||
|
const result = await scrapeUrl('https://geizhals.de/foo')
|
||||||
|
expect(result.price).toBe(42)
|
||||||
|
expect(fake.scrape).toHaveBeenCalledWith('https://geizhals.de/foo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws on unknown shop', async () => {
|
||||||
|
resetScrapersForTest()
|
||||||
|
await expect(scrapeUrl('https://example.com')).rejects.toThrow(/unsupported/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user