module Client.Shared.JsonWire // Fable.Remoting.Client pulls in Fable.Remoting.MsgPack, whose `inline` // helpers reference private functions in a way the current Fable compiler // rejects at build time. Until that's fixed upstream, each feature's `Api.fs` // talks to its own plain Giraffe route by hand, using the helpers below to // POST/GET JSON and decode the {"Ok": ...} / {"Error": ...} shape // Fable.Remoting.Json already produces on the server (see `Server/Json.fs`). open Fable.Core open Fable.Core.JsInterop open Domain // Relative on purpose — the server is always reached through whatever's // serving this page under `/api/*`: the client's own nginx in Docker // (see Client/nginx.conf), Vite's dev-server proxy (vite.config.js) for // `npm run dev`, or the host reverse proxy in production. This also means // the browser never makes a cross-origin request, so CORS never enters // into it for any of these setups. let private serverUrl = "" [] let hasKey (_o: obj) (_key: string) : bool = jsNative [ r.json())")>] let private fetchJson (_url: string) (_init: obj) : JS.Promise = jsNative /// `body = None` for GET-style calls with no request payload. let callApi (token: string option) (httpMethod: string) (path: string) (body: obj option) : Async = async { let headers = match token with | Some t -> createObj [ "Content-Type" ==> "application/json"; "Authorization" ==> ("Bearer " + t) ] | None -> createObj [ "Content-Type" ==> "application/json" ] let baseFields = [ "method" ==> httpMethod; "headers" ==> headers ] let fields = match body with | Some b -> baseFields @ [ "body" ==> JS.JSON.stringify b ] | None -> baseFields return! fetchJson (serverUrl + path) (createObj fields) |> Async.AwaitPromise } // ---- Id encode/decode: wire shape is {"CaseName": ""} ---- let encQuizId (QuizId g) : obj = createObj [ "QuizId" ==> string g ] let encTopicId (TopicId g) : obj = createObj [ "TopicId" ==> string g ] let encUserId (UserId g) : obj = createObj [ "UserId" ==> string g ] let encAttemptId (AttemptId g) : obj = createObj [ "AttemptId" ==> string g ] let encQuestionId (QuestionId g) : obj = createObj [ "QuestionId" ==> string g ] let encOptionId (OptionId g) : obj = createObj [ "OptionId" ==> string g ] let decUserId (o: obj) : UserId = UserId(System.Guid.Parse(o?UserId: string)) let decTopicId (o: obj) : TopicId = TopicId(System.Guid.Parse(o?TopicId: string)) let decQuizId (o: obj) : QuizId = QuizId(System.Guid.Parse(o?QuizId: string)) let decQuestionId (o: obj) : QuestionId = QuestionId(System.Guid.Parse(o?QuestionId: string)) let decOptionId (o: obj) : OptionId = OptionId(System.Guid.Parse(o?OptionId: string)) let decAttemptId (o: obj) : AttemptId = AttemptId(System.Guid.Parse(o?AttemptId: string)) let optToJs (mapper: 'a -> obj) (opt: 'a option) : obj = match opt with | Some x -> mapper x | None -> null let optDec (mapper: obj -> 'a) (raw: obj) : 'a option = if isNullOrUndefined raw then None else Some(mapper raw) let decodeRole (raw: obj) : Role = match unbox raw with | "Admin" -> Admin | "Teacher" -> Teacher | "Student" -> Student | other -> failwithf "Неизвестная роль: %s" other let decodeResult (decodeOk: obj -> 'a) (raw: obj) : Result<'a, string> = if hasKey raw "Ok" then Ok(decodeOk raw?Ok) else Error(unbox raw?Error)