module Web.Controller.Api.V2.OpenApi where -- GET /api/v2/openapi.json — OpenAPI 3.1 spec with live type registry enums -- GET /api/v2/openapi.yaml — YAML convenience alias -- GET /api/v2/docs — Swagger UI import Web.Types import Generated.Types import IHP.Prelude import IHP.ControllerPrelude import Data.Aeson (object, (.=), Array, toJSON) import qualified Data.Aeson as A import qualified Data.Vector as V import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.Yaml as Yaml -- yaml package import qualified Data.ByteString.Lazy as LBS import Application.Helper.TypeRegistry ( activeWidgetTypes, activeEventTypes, activeAnnotationCategories ) import Network.HTTP.Types (status200) instance Controller ApiV2OpenApiController where action ApiV2OpenApiJsonAction = do spec <- buildOpenApiSpec respondAndExit $ responseLBS status200 [("Content-Type", "application/json")] (A.encode spec) action ApiV2OpenApiYamlAction = do spec <- buildOpenApiSpec let yaml = Yaml.encode spec respondAndExit $ responseLBS status200 [("Content-Type", "application/yaml")] (LBS.fromStrict yaml) action ApiV2DocsAction = do respondAndExit $ responseLBS status200 [("Content-Type", "text/html; charset=utf-8")] swaggerUiHtml -- | Build the full OpenAPI 3.1 document from live registry data. buildOpenApiSpec :: (?modelContext :: ModelContext) => IO Value buildOpenApiSpec = do (fwWidgetTypes, ownedWidgetTypes) <- activeWidgetTypes let allWidgetTypes = fwWidgetTypes ++ ownedWidgetTypes eventTypes <- activeEventTypes annCats <- activeAnnotationCategories let wtEnum = toJSON $ map (.name) allWidgetTypes let etEnum = toJSON $ map (.name) eventTypes let acEnum = toJSON $ map (.name) annCats pure $ object [ "openapi" .= ("3.1.0" :: Text) , "info" .= object [ "title" .= ("Interaction Hub Framework API" :: Text) , "version" .= ("2.0" :: Text) , "description" .= ("IHF external API v2. For the human-readable contract see /contracts/functional/interaction-reporting-v1.md" :: Text) ] , "x-ihf-contract" .= ("/contracts/functional/interaction-reporting-v1.md" :: Text) , "servers" .= [object ["url" .= ("/api/v2" :: Text)]] , "paths" .= buildPaths , "components" .= object [ "schemas" .= object [ "WidgetType" .= object [ "type" .= ("string" :: Text) , "enum" .= wtEnum ] , "EventType" .= object [ "type" .= ("string" :: Text) , "enum" .= etEnum ] , "AnnotationCategory" .= object [ "type" .= ("string" :: Text) , "enum" .= acEnum ] , "PaginationMeta" .= object [ "type" .= ("object" :: Text) , "properties" .= object [ "page" .= object ["type" .= ("integer" :: Text)] , "per_page" .= object ["type" .= ("integer" :: Text)] , "total" .= object ["type" .= ("integer" :: Text)] ] ] , "Widget" .= widgetSchema , "InteractionEvent" .= interactionEventSchema , "Annotation" .= annotationSchema , "RequirementCandidate" .= rcSchema , "DecisionRecord" .= drSchema , "DeploymentRecord" .= depSchema , "OutcomeSignal" .= sigSchema , "OutcomeCorrelation" .= outcomeCorrelationSchema , "PatternPerformanceRecord" .= patternPerformanceSchema , "InstitutionalKnowledgeEntry" .= institutionalKnowledgeSchema ] , "securitySchemes" .= object [ "BearerAuth" .= object [ "type" .= ("http" :: Text) , "scheme" .= ("bearer" :: Text) , "description" .= ("API key or OAuth token obtained via POST /api/v2/token" :: Text) ] ] ] , "security" .= [object ["BearerAuth" .= ([] :: [Text])]] ] buildPaths :: Value buildPaths = object [ "/widgets" .= getListPath "Widget" , "/widgets/{id}" .= getShowPath "Widget" , "/interaction-events" .= object [ "get" .= listOp "InteractionEvent" [ ("widgetId", "string", "uuid") , ("eventType", "string", "") ] , "post" .= writeOp "InteractionEvent" "CreateInteractionEventRequest" ] , "/annotations" .= object [ "get" .= listOp "Annotation" [ ("widgetId", "string", "uuid") , ("category", "string", "") ] , "post" .= writeOp "Annotation" "CreateAnnotationRequest" ] , "/requirement-candidates" .= getListPath "RequirementCandidate" , "/requirement-candidates/{id}" .= getShowPath "RequirementCandidate" , "/decision-records" .= getListPath "DecisionRecord" , "/decision-records/{id}" .= getShowPath "DecisionRecord" , "/deployment-records" .= getListPath "DeploymentRecord" , "/deployment-records/{id}" .= getShowPath "DeploymentRecord" , "/outcome-signals" .= getListPath "OutcomeSignal" , "/outcome-signals/{id}" .= getShowPath "OutcomeSignal" , "/widget-types" .= publicListPath "WidgetTypeRegistry" , "/event-types" .= publicListPath "EventTypeRegistry" , "/annotation-categories" .= publicListPath "AnnotationCategoryRegistry" , "/token" .= tokenPath -- Phase 10 — Hub Registry and Widget Marketplace , "/hub-registry" .= getListPath "HubRegistryEntry" , "/hub-registry/{hubId}" .= getShowPath "HubRegistryEntry" , "/widget-patterns" .= getListPath "WidgetPattern" , "/widget-patterns/{id}" .= getShowPath "WidgetPattern" , "/widget-patterns/{id}/adopt" .= object [ "post" .= writeOp "PatternAdoption" "AdoptPatternRequest" ] ] getListPath :: Text -> Value getListPath schemaName = object [ "get" .= listOp schemaName [] ] getShowPath :: Text -> Value getShowPath schemaName = object [ "get" .= showOp schemaName ] listOp :: Text -> [(Text, Text, Text)] -> Value listOp schemaName extraParams = object [ "summary" .= ("List " <> schemaName) , "security" .= [object ["BearerAuth" .= ([] :: [Text])]] , "parameters" .= (pageParams ++ map toParam extraParams) , "responses" .= object [ "200" .= object [ "description" .= ("OK" :: Text) , "content" .= object [ "application/json" .= object [ "schema" .= object [ "type" .= ("object" :: Text) , "properties" .= object [ "data" .= object [ "type" .= ("array" :: Text) , "items" .= object ["$ref" .= ("#/components/schemas/" <> schemaName)] ] , "meta" .= object ["$ref" .= ("#/components/schemas/PaginationMeta" :: Text)] ] ] ] ] ] , "401" .= object ["description" .= ("Unauthorized" :: Text)] ] ] where toParam (name, typ, fmt) = object $ [ "name" .= name, "in" .= ("query" :: Text) , "schema" .= object (["type" .= typ] ++ if fmt /= "" then [("format", A.String fmt)] else []) ] showOp :: Text -> Value showOp schemaName = object [ "summary" .= ("Get " <> schemaName) , "security" .= [object ["BearerAuth" .= ([] :: [Text])]] , "parameters" .= [object ["name" .= ("id" :: Text), "in" .= ("path" :: Text), "required" .= True, "schema" .= object ["type" .= ("string" :: Text), "format" .= ("uuid" :: Text)]]] , "responses" .= object [ "200" .= object [ "description" .= ("OK" :: Text) , "content" .= object [ "application/json" .= object ["schema" .= object ["$ref" .= ("#/components/schemas/" <> schemaName)]] ] ] , "401" .= object ["description" .= ("Unauthorized" :: Text)] , "404" .= object ["description" .= ("Not found" :: Text)] ] ] writeOp :: Text -> Text -> Value writeOp schemaName _reqSchema = object [ "summary" .= ("Create " <> schemaName) , "security" .= [object ["BearerAuth" .= ([] :: [Text])]] , "requestBody" .= object [ "required" .= True , "content" .= object [ "application/json" .= object ["schema" .= object ["$ref" .= ("#/components/schemas/" <> schemaName)]] ] ] , "responses" .= object [ "201" .= object ["description" .= ("Created" :: Text)] , "401" .= object ["description" .= ("Unauthorized" :: Text)] , "422" .= object ["description" .= ("Validation error" :: Text)] ] ] publicListPath :: Text -> Value publicListPath schemaName = object [ "get" .= object [ "summary" .= ("List registered " <> schemaName <> " values" :: Text) , "responses" .= object [ "200" .= object ["description" .= ("OK" :: Text)] ] ] ] tokenPath :: Value tokenPath = object [ "post" .= object [ "summary" .= ("Obtain OAuth access token (client credentials)" :: Text) , "requestBody" .= object [ "required" .= True , "content" .= object [ "application/x-www-form-urlencoded" .= object [ "schema" .= object [ "type" .= ("object" :: Text) , "properties" .= object [ "grant_type" .= object ["type" .= ("string" :: Text), "enum" .= ["client_credentials" :: Text]] , "client_id" .= object ["type" .= ("string" :: Text), "format" .= ("uuid" :: Text)] , "client_secret" .= object ["type" .= ("string" :: Text)] , "scope" .= object ["type" .= ("string" :: Text)] ] ] ] ] ] , "responses" .= object [ "200" .= object ["description" .= ("Access token issued" :: Text)] , "400" .= object ["description" .= ("Invalid request or credentials" :: Text)] ] ] ] pageParams :: [Value] pageParams = [ object ["name" .= ("page" :: Text), "in" .= ("query" :: Text), "schema" .= object ["type" .= ("integer" :: Text)]] , object ["name" .= ("per_page" :: Text), "in" .= ("query" :: Text), "schema" .= object ["type" .= ("integer" :: Text)]] ] -- Schemas for all resource types widgetSchema :: Value widgetSchema = object [ "type" .= ("object" :: Text) , "properties" .= object [ "id" .= uuidProp , "hubId" .= uuidProp , "name" .= strProp , "widgetType" .= object ["$ref" .= ("#/components/schemas/WidgetType" :: Text)] , "capabilityRef" .= strProp , "viewContext" .= strProp , "policyScope" .= strProp , "status" .= strProp , "version" .= object ["type" .= ("integer" :: Text)] , "createdAt" .= object ["type" .= ("string" :: Text), "format" .= ("date-time" :: Text)] ] ] interactionEventSchema :: Value interactionEventSchema = object [ "type" .= ("object" :: Text) , "properties" .= object [ "id" .= uuidProp , "widgetId" .= uuidProp , "eventType" .= object ["$ref" .= ("#/components/schemas/EventType" :: Text)] , "actorId" .= uuidProp , "actorType" .= strProp , "viewContextRef" .= strProp , "metadata" .= object ["type" .= ("object" :: Text)] , "occurredAt" .= dtProp ] ] annotationSchema :: Value annotationSchema = object [ "type" .= ("object" :: Text) , "properties" .= object [ "id" .= uuidProp , "widgetId" .= uuidProp , "parentId" .= uuidProp , "body" .= strProp , "category" .= object ["$ref" .= ("#/components/schemas/AnnotationCategory" :: Text)] , "severity" .= strProp , "threadId" .= uuidProp , "actorId" .= uuidProp , "actorType" .= strProp , "createdAt" .= dtProp ] ] rcSchema :: Value rcSchema = object [ "type" .= ("object" :: Text) , "properties" .= object [ "id" .= uuidProp , "title" .= strProp , "description" .= strProp , "sourceWidgetId" .= uuidProp , "category" .= strProp , "status" .= strProp , "createdAt" .= dtProp ] ] drSchema :: Value drSchema = object [ "type" .= ("object" :: Text) , "properties" .= object [ "id" .= uuidProp , "title" .= strProp , "rationale" .= strProp , "outcome" .= strProp , "requirementId" .= uuidProp , "candidateId" .= uuidProp , "decidedAt" .= dtProp , "notes" .= strProp , "createdAt" .= dtProp ] ] depSchema :: Value depSchema = object [ "type" .= ("object" :: Text) , "properties" .= object [ "id" .= uuidProp , "decisionId" .= uuidProp , "versionRef" .= strProp , "deployedAt" .= dtProp , "notes" .= strProp , "createdAt" .= dtProp ] ] sigSchema :: Value sigSchema = object [ "type" .= ("object" :: Text) , "properties" .= object [ "id" .= uuidProp , "widgetId" .= uuidProp , "deploymentId" .= uuidProp , "signalType" .= strProp , "value" .= object ["type" .= ("number" :: Text)] , "observedAt" .= dtProp ] ] outcomeCorrelationSchema :: Value outcomeCorrelationSchema = object [ "type" .= ("object" :: Text) , "properties" .= object [ "id" .= uuidProp , "hubId" .= uuidProp , "annotationCategory" .= strProp , "correlationType" .= strProp , "correlationScore" .= object ["type" .= ("number" :: Text)] , "sampleCount" .= object ["type" .= ("integer" :: Text)] , "computedAt" .= dtProp ] ] patternPerformanceSchema :: Value patternPerformanceSchema = object [ "type" .= ("object" :: Text) , "properties" .= object [ "id" .= uuidProp , "widgetPatternId" .= uuidProp , "hubId" .= uuidProp , "adoptionCount" .= object ["type" .= ("integer" :: Text)] , "positiveOutcomeCount" .= object ["type" .= ("integer" :: Text)] , "totalOutcomeCount" .= object ["type" .= ("integer" :: Text)] , "positiveOutcomeRate" .= object ["type" .= ("number" :: Text)] , "meanOutcomeValue" .= object ["type" .= ("number" :: Text)] , "outcomeRank" .= object ["type" .= ("integer" :: Text)] , "calibratedAt" .= dtProp ] ] institutionalKnowledgeSchema :: Value institutionalKnowledgeSchema = object [ "type" .= ("object" :: Text) , "properties" .= object [ "id" .= uuidProp , "hubId" .= uuidProp , "decisionRecordId" .= uuidProp , "summary" .= strProp , "tags" .= object ["type" .= ("array" :: Text)] , "createdAt" .= dtProp , "updatedAt" .= dtProp ] ] uuidProp :: Value uuidProp = object ["type" .= ("string" :: Text), "format" .= ("uuid" :: Text)] strProp :: Value strProp = object ["type" .= ("string" :: Text)] dtProp :: Value dtProp = object ["type" .= ("string" :: Text), "format" .= ("date-time" :: Text)] -- | Embedded Swagger UI HTML using CDN assets (no build step required) swaggerUiHtml :: LBS.ByteString swaggerUiHtml = LBS.fromStrict $ TE.encodeUtf8 swaggerUiHtmlText swaggerUiHtmlText :: Text swaggerUiHtmlText = "" <> "IHF API v2 \x2014 Documentation" <> "" <> "" <> "
" <> "" <> "" <> ""