import std/options import std/tables import html/script import io/bufreader import io/bufwriter import loader/headers import monoucha/fromjs import monoucha/javascript import monoucha/jserror import monoucha/jstypes import monoucha/quickjs import types/blob import types/formdata import types/opt import types/referrer import types/url type HttpMethod* = enum hmGet = "GET" hmConnect = "CONNECT" hmDelete = "DELETE" hmHead = "HEAD" hmOptions = "OPTIONS" hmPatch = "PATCH" hmPost = "POST" hmPut = "PUT" hmTrace = "TRACE" RequestMode* = enum rmNoCors = "no-cors" rmSameOrigin = "same-origin" rmCors = "cors" rmNavigate = "navigate" rmWebsocket = "websocket" CORSAttribute* = enum caNoCors = "no-cors" caAnonymous = "anonymous" caUseCredentials = "use-credentials" type RequestOriginType* = enum rotClient, rotOrigin RequestOrigin* = object case t*: RequestOriginType of rotClient: discard of rotOrigin: origin*: Origin RequestWindowType* = enum rwtClient, rwtNoWindow, rwtWindow RequestWindow* = object case t*: RequestWindowType of rwtClient, rwtNoWindow: discard of rwtWindow: window*: EnvironmentSettings RequestBodyType* = enum rbtNone, rbtString, rbtMultipart, rbtOutput RequestBody* = object case t*: RequestBodyType of rbtNone: discard of rbtString: s*: string of rbtMultipart: multipart*: FormData of rbtOutput: outputId*: int Request* = ref object httpMethod*: HttpMethod url*: URL headers*: Headers body*: RequestBody referrer*: URL proxy*: URL #TODO do something with this JSRequest* = ref object request*: Request mode* {.jsget.}: RequestMode destination* {.jsget.}: RequestDestination credentialsMode* {.jsget.}: CredentialsMode origin*: RequestOrigin window*: RequestWindow client*: Option[EnvironmentSettings] jsDestructor(JSRequest) proc swrite*(writer: var BufferedWriter; o: RequestBody) = writer.swrite(o.t) case o.t of rbtNone: discard of rbtString: writer.swrite(o.s) of rbtMultipart: writer.swrite(o.multipart) of rbtOutput: writer.swrite(o.outputId) proc sread*(reader: var BufferedReader; o: var RequestBody) = var t: RequestBodyType reader.sread(t) o = RequestBody(t: t) case t of rbtNone: discard of rbtString: reader.sread(o.s) of rbtMultipart: reader.sread(o.multipart) of rbtOutput: reader.sread(o.outputId) proc contentLength*(body: RequestBody): int = case body.t of rbtNone: return 0 of rbtString: return body.s.len of rbtMultipart: return body.multipart.calcLength() of rbtOutput: return 0 func headers(this: JSRequest): Headers {.jsfget.} = return this.request.headers func url(this: JSRequest): URL = return this.request.url proc jsUrl(this: JSRequest): string {.jsfget: "url".} = return $this.url #TODO pretty sure this is incorrect proc jsReferrer(this: JSRequest): string {.jsfget: "referrer".} = if this.request.referrer != nil: return $this.request.referrer return "" iterator pairs*(headers: Headers): (string, string) = for k, vs in headers.table: for v in vs: yield (k, v) func newRequest*(url: URL; httpMethod = hmGet; headers = newHeaders(); body = RequestBody(); proxy: URL = nil; referrer: URL = nil): Request = return Request( url: url, httpMethod: httpMethod, headers: headers, body: body, referrer: referrer, proxy: proxy ) func createPotentialCORSRequest*(url: URL; destination: RequestDestination; cors: CORSAttribute; fallbackFlag = false): JSRequest = var mode = if cors == caNoCors: rmNoCors else: rmCors if fallbackFlag and mode == rmNoCors: mode = rmSameOrigin let credentialsMode = if cors == caAnonymous: cmSameOrigin else: cmInclude return JSRequest( request: newRequest(url), destination: destination, mode: mode, credentialsMode: credentialsMode ) type BodyInitType = enum bitBlob, bitFormData, bitUrlSearchParams, bitString BodyInit = object #TODO ReadableStream, BufferSource case t: BodyInitType of bitBlob: blob: Blob of bitFormData: formData: FormData of bitUrlSearchParams: searchParams: URLSearchParams of bitString: str: string RequestInit* = object of JSDict `method`* {.jsdefault.}: Option[HttpMethod] #TODO aliasing in dicts headers* {.jsdefault.}: Option[HeadersInit] body* {.jsdefault.}: Option[BodyInit] referrer* {.jsdefault.}: Option[string] referrerPolicy* {.jsdefault.}: Option[ReferrerPolicy] credentials* {.jsdefault.}: Option[CredentialsMode] mode* {.jsdefault.}: Option[RequestMode] window* {.jsdefault: JS_UNDEFINED.}: JSValue proc fromJS(ctx: JSContext; val: JSValue; res: var BodyInit): Opt[void] = if not JS_IsUndefined(val) and not JS_IsNull(val): res = BodyInit(t: bitFormData) if ctx.fromJS(val, res.formData).isSome: return ok() res = BodyInit(t: bitBlob) if ctx.fromJS(val, res.blob).isSome: return ok() res = BodyInit(t: bitUrlSearchParams) if ctx.fromJS(val, res.searchParams).isSome: return ok() res = BodyInit(t: bitString) if ctx.fromJS(val, res.str).isSome: return ok() JS_ThrowTypeError(ctx, "invalid body init type") return err() var getAPIBaseURLImpl*: proc(ctx: JSContext): URL {.noSideEffect, nimcall.} proc newRequest*(ctx: JSContext; resource: JSValue; init = RequestInit(window: JS_UNDEFINED)): JSResult[JSRequest] {.jsctor.} = let headers = newHeaders(hgRequest) var fallbackMode = opt(rmCors) var window = RequestWindow(t: rwtClient) var body = RequestBody() var credentials = cmSameOrigin var httpMethod = hmGet var referrer: URL = nil var url: URL = nil if (var res: JSRequest; ctx.fromJS(resource, res).isSome): url = res.url httpMethod = res.request.httpMethod headers.table = res.headers.table referrer = res.request.referrer credentials = res.credentialsMode body = res.request.body fallbackMode = opt(RequestMode) window = res.window else: var s: string ?ctx.fromJS(resource, s) url = ?parseJSURL(s, option(ctx.getAPIBaseURLImpl())) if url.username != "" or url.password != "": return errTypeError("Input URL contains a username or password") var mode = fallbackMode.get(rmNoCors) let destination = rdNone #TODO origin, window if not JS_IsUndefined(init.window): if not JS_IsNull(init.window): return errTypeError("Expected window to be null") window = RequestWindow(t: rwtNoWindow) if mode == rmNavigate: mode = rmSameOrigin #TODO flags? #TODO referrer if init.`method`.isSome: httpMethod = init.`method`.get if init.body.isSome: let ibody = init.body.get case ibody.t of bitFormData: body = RequestBody(t: rbtMultipart, multipart: ibody.formData) of bitString: body = RequestBody(t: rbtString, s: ibody.str) else: discard #TODO if httpMethod in {hmGet, hmHead}: return errTypeError("HEAD or GET Request cannot have a body.") if init.headers.isSome: ?headers.fill(init.headers.get) if init.credentials.isSome: credentials = init.credentials.get if init.mode.isSome: mode = init.mode.get if mode == rmNoCors: headers.guard = hgRequestNoCors return ok(JSRequest( request: newRequest( url, httpMethod, headers, body, referrer = referrer ), mode: mode, credentialsMode: credentials, destination: destination, window: window )) func credentialsMode*(attribute: CORSAttribute): CredentialsMode = case attribute of caNoCors, caAnonymous: return cmSameOrigin of caUseCredentials: return cmInclude proc addRequestModule*(ctx: JSContext) = ctx.registerType(JSRequest, name = "Request")