Initial commit: standalone quiz-testing system
Full-stack F# (Domain/Server/Client via Fable+Elmish+Feliz), PostgreSQL persistence via Dapper, Docker Compose deployment. Student quiz-taking flow with time-limit enforcement and focus-loss tracking, Teacher question bank and quiz builder with results analytics, Admin user management.
This commit is contained in:
71
src/Client/Shared/JsonWire.fs
Normal file
71
src/Client/Shared/JsonWire.fs
Normal file
@@ -0,0 +1,71 @@
|
||||
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
|
||||
|
||||
let private serverUrl = "http://localhost:5144"
|
||||
|
||||
[<Emit("Object.prototype.hasOwnProperty.call($0, $1)")>]
|
||||
let hasKey (_o: obj) (_key: string) : bool = jsNative
|
||||
|
||||
[<Emit("fetch($0, $1).then(r => r.json())")>]
|
||||
let private fetchJson (_url: string) (_init: obj) : JS.Promise<obj> = jsNative
|
||||
|
||||
/// `body = None` for GET-style calls with no request payload.
|
||||
let callApi (token: string option) (httpMethod: string) (path: string) (body: obj option) : Async<obj> =
|
||||
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": "<guid>"} ----
|
||||
|
||||
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<string> 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<string> raw?Error)
|
||||
Reference in New Issue
Block a user