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 createHub(body: { slug: string; name: string; domain: string; hubKind?: 'domain' | 'shared'; hubFamily?: 'vsm'; vsmFunction?: string; vsmSystem?: '1' | '2' | '3' | '3*' | '4' | '5' | 'environment' }) {" , " return this.fetch('/hubs', 'POST', body).then(r => r.json());" , " }" , "" , " async createHubCapabilityManifest(body: { hubId: string; manifestVersion?: string; declaredWidgetTypes?: WidgetType[]; declaredEventTypes?: EventType[]; declaredAnnotationCategories?: AnnotationCategory[]; declaredPolicyScopes?: string[]; capabilityDescription?: string; contact?: string }) {" , " return this.fetch('/hub-capability-manifests', 'POST', body).then(r => r.json());" , " }" , "" , " async updateHubCapabilityManifest(id: string, body: { manifestVersion?: string; declaredWidgetTypes?: WidgetType[]; declaredEventTypes?: EventType[]; declaredAnnotationCategories?: AnnotationCategory[]; declaredPolicyScopes?: string[]; capabilityDescription?: string; contact?: string }) {" , " return this.fetch('/hub-capability-manifests/' + id, 'PATCH', body).then(r => r.json());" , " }" , "" , " async activateHubCapabilityManifest(id: string) {" , " return this.fetch('/hub-capability-manifests/' + id + '/activate', 'POST').then(r => r.json());" , " }" , "" , " async createApiConsumer(body: { name: string; description?: string; hubCapabilityManifestId?: string; rateLimitPerMinute?: number; quotaPerDay?: number }) {" , " return this.fetch('/api-consumers', 'POST', body).then(r => r.json());" , " }" , "" , " async createApiKey(apiConsumerId: string, body?: { scopes?: string }) {" , " return this.fetch('/api-consumers/' + apiConsumerId + '/api-keys', 'POST', body ?? {}).then(r => r.json());" , " }" , "" , " 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 createWidget(body: { hubId: string; name: string; widgetType: WidgetType; capabilityRef?: string; viewContext?: string; policyScope?: string; status?: 'active' | 'deprecated' | 'draft' }) {" , " return this.fetch('/widgets', 'POST', body).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 create_hub(self, slug: str, name: str, domain: str, hub_kind: str = 'domain', hub_family: Optional[str] = None, vsm_function: Optional[str] = None, vsm_system: Optional[str] = None) -> dict:" , " body: dict = {'slug': slug, 'name': name, 'domain': domain, 'hubKind': hub_kind}" , " if hub_family: body['hubFamily'] = hub_family" , " if vsm_function: body['vsmFunction'] = vsm_function" , " if vsm_system: body['vsmSystem'] = vsm_system" , " return self._request('/hubs', 'POST', body)" , "" , " def create_hub_capability_manifest(self, body: dict) -> dict:" , " return self._request('/hub-capability-manifests', 'POST', body)" , "" , " def update_hub_capability_manifest(self, manifest_id: str, body: dict) -> dict:" , " return self._request('/hub-capability-manifests/' + manifest_id, 'PATCH', body)" , "" , " def activate_hub_capability_manifest(self, manifest_id: str) -> dict:" , " return self._request('/hub-capability-manifests/' + manifest_id + '/activate', 'POST')" , "" , " def create_api_consumer(self, name: str, description: Optional[str] = None, hub_capability_manifest_id: Optional[str] = None, rate_limit_per_minute: Optional[int] = None, quota_per_day: Optional[int] = None) -> dict:" , " body: dict = {'name': name}" , " if description: body['description'] = description" , " if hub_capability_manifest_id: body['hubCapabilityManifestId'] = hub_capability_manifest_id" , " if rate_limit_per_minute: body['rateLimitPerMinute'] = rate_limit_per_minute" , " if quota_per_day: body['quotaPerDay'] = quota_per_day" , " return self._request('/api-consumers', 'POST', body)" , "" , " def create_api_key(self, api_consumer_id: str, scopes: Optional[str] = None) -> dict:" , " body: dict = {}" , " if scopes: body['scopes'] = scopes" , " return self._request('/api-consumers/' + api_consumer_id + '/api-keys', 'POST', body)" , "" , " def get_widgets(self, page: int = 1, per_page: int = 50) -> dict:" , " return self._request(f'/widgets?page={page}&per_page={per_page}')" , "" , " def create_widget(self, hub_id: str, name: str, widget_type: WidgetType, capability_ref: Optional[str] = None, view_context: Optional[str] = None, policy_scope: Optional[str] = None, status: Optional[str] = None) -> dict:" , " body: dict = {'hubId': hub_id, 'name': name, 'widgetType': str(widget_type)}" , " if capability_ref: body['capabilityRef'] = capability_ref" , " if view_context: body['viewContext'] = view_context" , " if policy_scope: body['policyScope'] = policy_scope" , " if status: body['status'] = status" , " return self._request('/widgets', 'POST', body)" , "" , " 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.

" , "" ]