-
Notifications
You must be signed in to change notification settings - Fork 14
feat: typed Google Search parameters and responses (fixes #33) #37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| /** Parameters for the Google Search engine. */ | ||
| export interface GoogleSearchParameters { | ||
| engine: "google"; | ||
| q: string; | ||
| api_key?: string; | ||
| timeout?: number; | ||
|
|
||
| // Geographic Location | ||
| location?: string; | ||
| uule?: string; | ||
| lat?: number; | ||
| lon?: number; | ||
| radius?: number; | ||
|
|
||
| // Localization | ||
| google_domain?: string; | ||
| gl?: string; | ||
| hl?: string; | ||
| cr?: string; | ||
| lr?: string; | ||
|
|
||
| // Search Type | ||
| tbm?: "isch" | "lcl" | "vid" | "nws" | "shop" | "pts"; | ||
|
|
||
| // Pagination | ||
| start?: number; | ||
| num?: number; | ||
|
|
||
| // Advanced Filters | ||
| tbs?: string; | ||
| safe?: "active" | "off"; | ||
| nfpr?: 0 | 1; | ||
| filter?: 0 | 1; | ||
|
|
||
| // Advanced Google Parameters | ||
| ludocid?: string; | ||
| lsig?: string; | ||
| kgmid?: string; | ||
| si?: string; | ||
|
|
||
| // SerpApi Parameters | ||
| device?: "desktop" | "tablet" | "mobile"; | ||
| no_cache?: boolean; | ||
| async?: boolean; | ||
|
|
||
| // deno-lint-ignore no-explicit-any | ||
| [key: string]: any; | ||
| } | ||
|
|
||
| /** Metadata returned with every SerpApi response. */ | ||
| export interface SearchMetadata { | ||
| id: string; | ||
| status: string; | ||
| json_endpoint: string; | ||
| created_at: string; | ||
| processed_at: string; | ||
| google_url: string; | ||
| raw_html_file: string; | ||
| total_time_taken: number; | ||
| } | ||
|
|
||
| /** Echoed search parameters in the response. */ | ||
| export interface SearchParameters { | ||
| engine: string; | ||
| q: string; | ||
| google_domain?: string; | ||
| hl?: string; | ||
| gl?: string; | ||
| device?: string; | ||
| location_requested?: string; | ||
| location_used?: string; | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| /** Search result info. */ | ||
| export interface SearchInformation { | ||
| query_displayed: string; | ||
| total_results: number; | ||
| time_taken_displayed: number; | ||
| organic_results_state: string; | ||
| page_number?: number; | ||
| } | ||
|
|
||
| /** A single organic search result. */ | ||
| export interface OrganicResult { | ||
| position: number; | ||
| title: string; | ||
| link: string; | ||
| redirect_link?: string; | ||
| displayed_link: string; | ||
| snippet: string; | ||
| snippet_highlighted_words?: string[]; | ||
| date?: string; | ||
| sitelinks?: { | ||
| inline?: { title: string; link: string }[]; | ||
| expanded?: { title: string; link: string; snippet: string }[]; | ||
| }; | ||
| rich_snippet?: Record<string, unknown>; | ||
| source?: string; | ||
| } | ||
|
|
||
| /** Knowledge graph result. */ | ||
| export interface KnowledgeGraph { | ||
| title?: string; | ||
| type?: string; | ||
| description?: string; | ||
| source?: { name: string; link: string }; | ||
| [key: string]: unknown; | ||
| } | ||
|
|
||
| /** "People also ask" question. */ | ||
| export interface RelatedQuestion { | ||
| question: string; | ||
| snippet?: string; | ||
| title?: string; | ||
| link?: string; | ||
| } | ||
|
|
||
| /** Google Search JSON response. */ | ||
| export interface GoogleSearchResponse { | ||
| search_metadata: SearchMetadata; | ||
| search_parameters: SearchParameters; | ||
| search_information?: SearchInformation; | ||
| organic_results?: OrganicResult[]; | ||
| knowledge_graph?: KnowledgeGraph; | ||
| related_questions?: RelatedQuestion[]; | ||
| pagination?: { | ||
| current: number; | ||
| next?: string; | ||
| other_pages?: Record<string, string>; | ||
| }; | ||
| [key: string]: unknown; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ import { | |
| BaseResponse, | ||
| EngineParameters, | ||
| GetBySearchIdParameters, | ||
| GoogleSearchParameters, | ||
| GoogleSearchResponse, | ||
| LocationsApiParameters, | ||
| } from "./types.ts"; | ||
| import { _internals } from "./utils.ts"; | ||
|
|
@@ -14,6 +16,14 @@ const LOCATIONS_PATH = "/locations.json"; | |
| const SEARCH_PATH = "/search"; | ||
| const SEARCH_ARCHIVE_PATH = `/searches`; | ||
|
|
||
| /** | ||
| * Get typed JSON response for Google Search. | ||
| */ | ||
| export function getJson( | ||
| parameters: GoogleSearchParameters, | ||
| callback?: (json: GoogleSearchResponse) => void, | ||
| ): Promise<GoogleSearchResponse>; | ||
|
|
||
| /** | ||
| * Get JSON response based on search parameters. | ||
| * | ||
|
|
@@ -52,13 +62,15 @@ export function getJson( | |
|
|
||
| export function getJson( | ||
| ...args: | ||
| | [parameters: EngineParameters, callback?: (json: BaseResponse) => void] | ||
| // deno-lint-ignore no-explicit-any | ||
| | [parameters: EngineParameters, callback?: (json: any) => void] | ||
| | [ | ||
| engine: string, | ||
| parameters: EngineParameters, | ||
| callback?: (json: BaseResponse) => void, | ||
| // deno-lint-ignore no-explicit-any | ||
| callback?: (json: any) => void, | ||
| ] | ||
| ): Promise<BaseResponse> { | ||
| ): Promise<BaseResponse | GoogleSearchResponse> { | ||
|
Comment on lines
63
to
+73
|
||
| if (typeof args[0] === "string" && typeof args[1] === "object") { | ||
| const [engine, parameters, callback] = args; | ||
| const newParameters = { ...parameters, engine } as EngineParameters; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| /** | ||
| * Type-level tests for engine-specific types. | ||
| * These verify compile-time behavior — if this file fails to type-check, | ||
| * the types are broken. | ||
| */ | ||
| import type { | ||
| EngineParameters, | ||
| GoogleSearchParameters, | ||
| GoogleSearchResponse, | ||
| OrganicResult, | ||
| } from "../src/types.ts"; | ||
| import { getJson } from "../src/serpapi.ts"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| import { | ||
catherine-serpapi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| describe, | ||
| it, | ||
| } from "https://deno.land/std@0.170.0/testing/bdd.ts"; | ||
| import { | ||
| assertEquals, | ||
| } from "https://deno.land/std@0.170.0/testing/asserts.ts"; | ||
|
|
||
| describe("GoogleSearchParameters", () => { | ||
| it("accepts valid Google Search params", () => { | ||
| const params: GoogleSearchParameters = { | ||
| engine: "google", | ||
| q: "coffee", | ||
| location: "Austin, Texas", | ||
| gl: "us", | ||
| hl: "en", | ||
| num: 10, | ||
| start: 0, | ||
| safe: "active", | ||
| device: "desktop", | ||
| }; | ||
| assertEquals(params.engine, "google"); | ||
| assertEquals(params.q, "coffee"); | ||
| }); | ||
|
|
||
| it("allows tbm search type values", () => { | ||
| const params: GoogleSearchParameters = { | ||
| engine: "google", | ||
| q: "coffee", | ||
| tbm: "nws", | ||
| }; | ||
| assertEquals(params.tbm, "nws"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("GoogleSearchResponse", () => { | ||
| it("has correct shape", () => { | ||
| const response: GoogleSearchResponse = { | ||
| search_metadata: { | ||
| id: "123", | ||
| status: "Success", | ||
| json_endpoint: "https://serpapi.com/searches/123.json", | ||
| created_at: "2025-01-01", | ||
| processed_at: "2025-01-01", | ||
| google_url: "https://www.google.com/search?q=coffee", | ||
| raw_html_file: "https://serpapi.com/searches/123.html", | ||
| total_time_taken: 1.5, | ||
| }, | ||
| search_parameters: { | ||
| engine: "google", | ||
| q: "coffee", | ||
| }, | ||
| organic_results: [ | ||
| { | ||
| position: 1, | ||
| title: "Coffee - Wikipedia", | ||
| link: "https://en.wikipedia.org/wiki/Coffee", | ||
| displayed_link: "en.wikipedia.org", | ||
| snippet: "Coffee is a brewed drink...", | ||
| }, | ||
| ], | ||
| }; | ||
| assertEquals(response.search_metadata.status, "Success"); | ||
|
|
||
| const firstResult: OrganicResult = response.organic_results![0]; | ||
| assertEquals(firstResult.position, 1); | ||
| assertEquals(firstResult.title, "Coffee - Wikipedia"); | ||
| }); | ||
| }); | ||
|
|
||
| describe("backwards compatibility", () => { | ||
| it("generic EngineParameters still works", () => { | ||
| const params: EngineParameters = { | ||
| engine: "bing", | ||
| q: "anything", | ||
| custom_field: 123, | ||
| }; | ||
| assertEquals(params.engine, "bing"); | ||
| }); | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.