module Web.Controller.Api.V2.Sdk where -- GET /api/v2/sdk — SDK index page -- GET /api/v2/sdk/ihf-client.ts — TypeScript SDK (live-generated from registries) -- GET /api/v2/sdk/ihf-client.py — Python SDK (live-generated from registries) import Web.Types import Generated.Types import IHP.Prelude import IHP.ControllerPrelude import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.ByteString.Lazy as LBS import Network.HTTP.Types (status200) import Network.Wai (responseLBS) import Application.Helper.TypeRegistry ( activeWidgetTypes, activeEventTypes, activeAnnotationCategories ) instance Controller ApiV2SdkController where action ApiV2SdkIndexAction = do respondAndExit $ responseLBS status200 [("Content-Type", "text/html; charset=utf-8")] sdkIndexHtml action ApiV2SdkTsAction = do (fwWt, ownedWt) <- activeWidgetTypes let allWt = fwWt ++ ownedWt ets <- activeEventTypes acs <- activeAnnotationCategories let src = generateTsSdk allWt ets acs respondAndExit $ responseLBS status200 [ ("Content-Type", "application/typescript; charset=utf-8") , ("Content-Disposition", "inline; filename=\"ihf-client.ts\"") ] (LBS.fromStrict (TE.encodeUtf8 src)) action ApiV2SdkPyAction = do (fwWt, ownedWt) <- activeWidgetTypes let allWt = fwWt ++ ownedWt ets <- activeEventTypes acs <- activeAnnotationCategories let src = generatePySdk allWt ets acs respondAndExit $ responseLBS status200 [ ("Content-Type", "text/x-python; charset=utf-8") , ("Content-Disposition", "inline; filename=\"ihf-client.py\"") ] (LBS.fromStrict (TE.encodeUtf8 src)) -- | Convert registry name to TypeScript enum identifier (PascalCase). -- e.g. "data-table" -> "DataTable", "ux-friction" -> "UxFriction" toPascalCase :: Text -> Text toPascalCase = T.concat . map capitalise . T.splitOn "-" where capitalise "" = "" capitalise t = T.toUpper (T.take 1 t) <> T.drop 1 t -- | Convert registry name to Python UPPER_SNAKE identifier. -- e.g. "data-table" -> "DATA_TABLE", "ux-friction" -> "UX_FRICTION" toUpperSnake :: Text -> Text toUpperSnake = T.toUpper . T.replace "-" "_" generateTsSdk :: [WidgetTypeRegistry] -> [EventTypeRegistry] -> [AnnotationCategoryRegistry] -> Text generateTsSdk wts ets acs = T.unlines [ "// Auto-generated by IHF. Do not edit manually." , "// Regenerate: curl /api/v2/sdk/ihf-client.ts > ihf-client.ts" , "" , "export enum WidgetType {" ] <> T.unlines (map (\r -> " " <> toPascalCase r.name <> " = \"" <> r.name <> "\",") wts) <> "}\n\nexport enum EventType {\n" <> T.unlines (map (\r -> " " <> toPascalCase r.name <> " = \"" <> r.name <> "\",") ets) <> "}\n\nexport enum AnnotationCategory {\n" <> T.unlines (map (\r -> " " <> toPascalCase r.name <> " = \"" <> r.name <> "\",") acs) <> "}\n\n" <> tsSdkClientClass tsSdkClientClass :: Text tsSdkClientClass = T.unlines [ "export interface IhfApiOptions {" , " baseUrl: string;" , " bearerToken: string;" , "}" , "" , "export class IhfClient {" , " constructor(private opts: IhfApiOptions) {}" , "" , " private async fetch(path: string, method = 'GET', body?: object): Promise {" , " return fetch(this.opts.baseUrl + path, {" , " method," , " headers: {" , " 'Authorization': 'Bearer ' + this.opts.bearerToken," , " 'Content-Type': 'application/json'," , " }," , " body: body ? JSON.stringify(body) : undefined," , " });" , " }" , "" , " async getWidgets(params?: { page?: number; perPage?: number }) {" , " const q = params ? `?page=${params.page ?? 1}&per_page=${params.perPage ?? 50}` : '';" , " return this.fetch('/widgets' + q).then(r => r.json());" , " }" , "" , " async getInteractionEvents(params?: { widgetId?: string; eventType?: EventType }) {" , " const qs = new URLSearchParams();" , " if (params?.widgetId) qs.set('widgetId', params.widgetId);" , " if (params?.eventType) qs.set('eventType', params.eventType);" , " return this.fetch('/interaction-events?' + qs).then(r => r.json());" , " }" , "" , " async submitInteractionEvent(body: { widgetId: string; eventType: EventType; viewContext?: string; metadata?: object }) {" , " return this.fetch('/interaction-events', 'POST', body).then(r => r.json());" , " }" , "" , " async submitAnnotation(body: { widgetId: string; category: AnnotationCategory; body: string }) {" , " return this.fetch('/annotations', 'POST', body).then(r => r.json());" , " }" , "}" ] generatePySdk :: [WidgetTypeRegistry] -> [EventTypeRegistry] -> [AnnotationCategoryRegistry] -> Text generatePySdk wts ets acs = T.unlines [ "# Auto-generated by IHF. Do not edit manually." , "# Regenerate: curl /api/v2/sdk/ihf-client.py > ihf_client.py" , "" , "from enum import Enum" , "from typing import Optional, Any" , "import urllib.request, urllib.parse, json" , "" , "class WidgetType(str, Enum):" ] <> T.unlines (map (\r -> " " <> toUpperSnake r.name <> " = \"" <> r.name <> "\"") wts) <> "\nclass EventType(str, Enum):\n" <> T.unlines (map (\r -> " " <> toUpperSnake r.name <> " = \"" <> r.name <> "\"") ets) <> "\nclass AnnotationCategory(str, Enum):\n" <> T.unlines (map (\r -> " " <> toUpperSnake r.name <> " = \"" <> r.name <> "\"") acs) <> "\n" <> pyClientClass pyClientClass :: Text pyClientClass = T.unlines [ "class IhfClient:" , " def __init__(self, base_url: str, bearer_token: str):" , " self.base_url = base_url.rstrip('/')" , " self.token = bearer_token" , "" , " def _request(self, path: str, method: str = 'GET', body: Optional[dict] = None) -> dict:" , " url = self.base_url + path" , " data = json.dumps(body).encode('utf-8') if body else None" , " req = urllib.request.Request(url, data=data, method=method," , " headers={'Authorization': 'Bearer ' + self.token, 'Content-Type': 'application/json'})" , " with urllib.request.urlopen(req) as resp:" , " return json.loads(resp.read())" , "" , " def get_widgets(self, page: int = 1, per_page: int = 50) -> dict:" , " return self._request(f'/widgets?page={page}&per_page={per_page}')" , "" , " def get_interaction_events(self, widget_id: Optional[str] = None, event_type: Optional[EventType] = None) -> dict:" , " qs = urllib.parse.urlencode({k: v for k, v in {'widgetId': widget_id, 'eventType': event_type and str(event_type)}.items() if v})" , " return self._request('/interaction-events' + ('?' + qs if qs else ''))" , "" , " def submit_interaction_event(self, widget_id: str, event_type: EventType, view_context: Optional[str] = None, metadata: Optional[dict] = None) -> dict:" , " body: dict = {'widgetId': widget_id, 'eventType': str(event_type)}" , " if view_context: body['viewContext'] = view_context" , " if metadata: body['metadata'] = metadata" , " return self._request('/interaction-events', 'POST', body)" , "" , " def submit_annotation(self, widget_id: str, category: AnnotationCategory, body: str) -> dict:" , " return self._request('/annotations', 'POST', {'widgetId': widget_id, 'category': str(category), 'body': body})" ] sdkIndexHtml :: LBS.ByteString sdkIndexHtml = LBS.fromStrict $ TE.encodeUtf8 $ T.unlines [ "" , "IHF API v2 — SDKs" , "" , "

IHF Consumer SDKs

" , "

Both SDKs are generated live from the type registries. Download and import directly.

" , "
" , "
" , "

TypeScript / Node.js

" , "

ES2020 module. Typed enums for all widget types, event types, annotation categories.

" , "Download ihf-client.ts" , "
" , "
" , "

Python

" , "

stdlib-only (no third-party deps). str-Enum classes for all registered types.

" , "Download ihf-client.py" , "
" , "
" , "

See API documentation for full endpoint reference.

" , "" ]