From adc4e5724c926cac4ecb8dc3a58a5f9b4d1b9b81 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Mon, 28 Sep 2026 04:51:16 +0100 Subject: Take a view's arguments from the request's query GET /views/name?argument=value gives a view that takes arguments its arguments: the query as it came is kept on the request's work slot (it outlives the connection, which may go before the view is shown), and when the view is shown, on the connection thread from a snapshot or by the owner, its pairs (values %XX decoded) are laid out as a payload of the arguments' shape and given before the view is evaluated. from and count still page a table. An argument missing, unknown, given twice, not of its type or badly escaped answers 400 invalid. A registered query takes its view's arguments the same way (GET /queries/name?k=4). An arguments input is not an input to post to. server_arguments opens 200 accounts, then asks 83 times against a model: balances by key (404 no_value for a key not there), counts above an argument, texts with &, =, % and non-ASCII shown back, a table filtered by two arguments whose column folds with one, paged, counts by an owner's name, a registered query with an argument, and six refusals. The format's document (008) describes arguments. --- docs/design/008-module-format.md | 17 ++++-- serve/serve.c | 125 +++++++++++++++++++++++++++++++++++---- tests/check.py | 102 ++++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 14 deletions(-) diff --git a/docs/design/008-module-format.md b/docs/design/008-module-format.md index 4b9855c..0e503cf 100644 --- a/docs/design/008-module-format.md +++ b/docs/design/008-module-format.md @@ -14,8 +14,8 @@ v4는 **레코드**, 레코드의 **리스트** 상태, 리스트의 **인덱스 v5는 값을 가진 채 합류하는 분기(조건식)와, 리스트 항목마다 행을 내는 **표 보기**, 한 번 계산한 값을 여러 번 쓰게 하는 스택 복사(PICK·SLIDE, 툴체인의 함수 인라인용)를 더한다. 이전 판은 받지 않는다(생산자는 aup 하나다). v5에는 뒤에 집계(FOLD), 걸러진 표, 마크, 그리고 **키 있는 리스트**와 text 비교가 더해졌다. 그 뒤 핸들러에서 리스트를 -도는 **걷기**(EACH)와 **메시지의 행**(레코드의 리스트인 메시지 필드와, 그 행을 담는 행 목록 상태)이 더해졌다. 모두 전의 -v5 모듈이 그대로 유효하도록 더한 것이다(새 명령과 이전에 0이던 자리의 값). +도는 **걷기**(EACH)와 **메시지의 행**(레코드의 리스트인 메시지 필드와, 그 행을 담는 행 목록 상태), 그리고 **인자를 받는 +보기**가 더해졌다. 모두 전의 v5 모듈이 그대로 유효하도록 더한 것이다(새 명령, 새 종류, 이전에 0이던 자리의 값). ## 바이트 배치 @@ -30,7 +30,7 @@ v5 모듈이 그대로 유효하도록 더한 것이다(새 명령과 이전에 | 32 | u32 상수 블롭 바이트 수, u16 레코드 필드, u16 0 | | 40 | 상태 × 16 B: u32 이름, u32 타입(0 수, 1 text, 2 커서, 3 리스트, 4 인덱스), 8 B 값: 수는 f64 초기값, text는 u32 초기 상수, 커서는 u32 대상 text 상태와 u32 플래그(1이면 마크: 그 자리에 넣은 글 앞에 남는다), 리스트는 u32 레코드와 u32 키(0이면 없음, 아니면 키 필드 + 1, 256이면 메시지의 행 목록: 이름은 그 필드의 경로 `입력.필드`이고 키가 없다), 인덱스는 u32 대상 리스트 상태 | | | 컬렉션 × 12 B: u32 이름, u32 1(바이트), u32 1(키별) | -| | 입력 × 16 B: u32 이름, u8 종류(1 키, 2 메시지, 3 text, 4 타이머, 5 출력: 앱이 보내는 메시지, 모양은 메시지와 같고 핸들러가 없다), u8 키별 여부, u16 0, u32 키 이름(키 입력)·주기(타이머, 1 ms–24시간을 ms로) 또는 0, u16 첫 필드, u16 필드 수 | +| | 입력 × 16 B: u32 이름, u8 종류(1 키, 2 메시지, 3 text, 4 타이머, 5 출력: 앱이 보내는 메시지, 모양은 메시지와 같고 핸들러가 없다, 6 인자: 한 보기가 받는 인자, 모양은 메시지와 같고 핸들러가 없으며 이름은 `보기.args`), u8 키별 여부, u16 0, u32 키 이름(키 입력)·주기(타이머, 1 ms–24시간을 ms로)·보기 번호(인자) 또는 0, u16 첫 필드, u16 필드 수 | | | 필드 × 8 B: u32 이름(식별자, 또는 그룹 안의 필드면 점으로 이은 경로 `user.name`), u32 타입(1 u32le, 2 바이트, 3 수(binary64), 4 text, 5 행: 비트 8–23에 그 행이 채우는 행 목록 상태). 페이로드는 접두부와 가변 필드의 바이트다. 접두부에는 선언 순서로 고정 필드의 값과 가변 필드(바이트·text)의 u32 길이가 들어가는데, 마지막 가변 필드는 길이 없이 나머지를 차지한다. 가변 필드의 바이트는 접두부 뒤에 선언 순서로 이어진다(가변 필드가 하나면 예전의 "바이트 꼬리"와 같다) | | | 상수 × 8 B: u32 블롭 오프셋, u32 길이 (UTF-8) | | | 레코드 × 8 B: u32 이름, u16 첫 레코드 필드, u16 필드 수(1–16) | @@ -61,7 +61,7 @@ v5 모듈이 그대로 유효하도록 더한 것이다(새 명령과 이전에 | 20 | STORE | n → | 상태 `인자`. 핸들러 전용 | | 21 | JZ | n → | 0이면 `인자`로 | | 22 | JMP | | `인자`로 | -| 23 | FIELD | → n, b 또는 t | 메시지 필드 `인자`(u32·수는 n, 바이트는 b, text는 t). 메시지 핸들러 전용. 도착할 때 길이가 페이로드를 넘거나 text가 UTF-8이 아니거나 수가 유한하지 않으면 메시지가 `limit`·`encoding`·`non_finite`로 실패한다 | +| 23 | FIELD | → n, b 또는 t | 메시지 필드 `인자`(u32·수는 n, 바이트는 b, text는 t). 메시지 핸들러와 그 안의 집계, 그리고 인자를 받는 보기(그 표의 열과 필터, 그 안의 집계)에서 인자를 읽는다. 도착할 때 길이가 페이로드를 넘거나 text가 UTF-8이 아니거나 수가 유한하지 않으면 메시지가 `limit`·`encoding`·`non_finite`로 실패한다 | | 24 | DOC | → b | 컬렉션 `인자`의 메시지 키 문서. 없으면 `not_found`. 키별 메시지 핸들러 전용 | | 25 | DOC_SIZE | → n | 그 문서의 바이트 수(없으면 0) | | 26 | LEN | b → n | 바이트 값의 길이 | @@ -148,6 +148,9 @@ v5 모듈이 그대로 유효하도록 더한 것이다(새 명령과 이전에 블록 안으로 들어오지 못한다. 걷는 리스트는 줄기만 하므로 한 걷기는 시작할 때의 항목 수 × 블록 길이를 넘지 않는다. - 행 필드(타입 5)는 메시지 입력에만 있고(출력은 행을 보내지 않는다), 가리키는 상태는 행 목록이며 그 이름이 `입력 이름.필드 경로`와 같다. 그래서 행 목록 하나는 필드 하나가 채운다. FIELD는 행 필드를 읽지 않는다(행은 리스트로 읽는다). +- 인자(종류 6)는 열이 아닌 보기 하나를 가리키고, 보기마다 하나이며, 이름이 `보기 이름.args`다. 핸들러를 갖지 않는다. 그 + 보기와(표라면 그 열과 필터도) 그 안의 집계는 FIELD로 인자를 읽는다. 집계 블록은 둘레 블록의 메시지나 인자를 읽을 수 있다 + (읽기뿐: 응답과 문서 연산은 핸들러 자신의 코드에만 있다). - 표는 리스트 상태를 가리키고 열은 앞에 나온 표를 가리킨다. 표 밖의 이름은 한 이름공간이고, 열 이름은 표 안에서만 유일하다. @@ -174,6 +177,11 @@ text, 끝이 맞지 않는 바이트는 메시지를 `limit`으로 실패시킨 읽고(걷기, 집계, `count`), 한 이벤트 안의 임시 표로 쓸 수도 있다. 한 메시지의 모든 행은 한 트랜잭션이다. 행 하나가 실패하면 (없는 키, `require`) 어느 행도 적용되지 않는다. +**인자를 받는 보기.** 호스트가 보기의 인자를 메시지 페이로드처럼 주면(`pure_app_arguments`: 고정 필드는 접두부, text와 바이트는 +그 뒤) 그 보기와 열, 행이 다시 줄 때까지 그것을 읽는다. 주지 않은 보기는 값이 없다. 걸러진 표의 행은 준 인자마다 다시 거른다. +인자는 잎 단위 실행에서 모두에 같은 값이고, 한 번의 통과로 답하는 열의 집계(PureShape)에서는 행이 주는 키 쪽에만 올 수 있다 +(통과는 인자에 따라 달라지지 않는다). + **걷기.** EACH는 걷는 항목의 위치를 인덱스처럼 들고 블록을 항목마다 돌린다. 블록이 걷는 리스트에서 항목을 지우면(LIST_REMOVE) 그 위치가 걷는 항목 앞이면 한 칸 당기고, 걷는 항목 자신이면 다음 차례에 같은 위치(곧 다음 항목)를 돈다. 블록은 걷는 리스트에 넣을 수 없으므로 걷기는 시작할 때 있던 항목 가운데 차례가 올 때까지 남은 것을 한 번씩, 순서대로 돈다. 블록이 실패하면 이벤트가 @@ -309,6 +317,7 @@ text·리스트가 남을 때만 위치를 유지한다. |---|---| | `GET /views/이름` | 보기의 값. 수는 가장 짧은 왕복 10진수, text는 UTF-8 그대로(`text/plain`). text 상태를 그대로 보이는 보기(`view page = note`)는 저장된 익스텐트를 빌려준다([079](../research/079-lent-views.md)) | | `GET /views/표?from=N&count=M` | 표의 행: `{"rows":전체,"from":N,"items":[{열: 값}…]}`(`application/json`). 요청한 행의 칸만 계산한다. 값이 없는 칸은 `null` | +| `GET /views/이름?인자=값…` | 인자를 받는 보기(표라면 `from`·`count`와 함께). 값은 `%XX`를 풀어 읽고, 고정 필드는 메시지의 쿼리 필드와 같다. 빠진 고정 인자, 모르는 이름, 두 번 준 인자, 타입이 다른 값은 400 `invalid`. 등록한 질의도 같다(`GET /queries/이름?인자=값`) | | `POST /inputs/이름` | 이벤트. text 입력은 본문이 text, 키 입력은 누름(down 뒤 up). 메시지는 고정 필드(u32, 수)를 쿼리(`?points=3`, 그룹 안이면 `?to.zip=7`)로, 가변 필드 하나를 본문으로 받거나, `Content-Type: application/json` 본문의 객체로 필드를 받는다(문자열은 바이트·text, 수는 u32·수, 그룹은 안의 객체). 응답 본문은 핸들러의 `reply`(메시지 필드) | | `POST /inputs/이름` + `X-Pure-Source`·`X-Pure-Sequence` | 다른 앱의 출력에서 전해진 메시지(`pure_app_deliver`): 번호마다 한 번 적용하고, 이미 들은 번호는 200. 답에 `X-Pure-Batch: 256`을 싣고, `X-Pure-Count: N`이면 본문은 N개 객체의 JSON 배열(연속 번호, 한 묶음에서 차례로 적용) | | `GET`·`PUT /objects/키` | 모듈에 키별 메시지 `show`·`edit`가 있으면 기존 문서 서버(`server.c`)와 같은 계약. `GET`은 `show`, `PUT`(`If-Match`/`If-None-Match`)은 `edit` | diff --git a/serve/serve.c b/serve/serve.c index 7fb3768..b9b526b 100644 --- a/serve/serve.c +++ b/serve/serve.c @@ -70,7 +70,9 @@ typedef struct Host Host; * an input. Kept per work slot, so an accepted event runs even if its * connection is gone before the burst. */ typedef enum { ROUTE_NONE, ROUTE_OBJECT, ROUTE_VIEW, ROUTE_INPUT, ROUTE_QUERY } RouteKind; -typedef struct { RouteKind kind; uint32_t target; size_t from, count; } Route; +/* A view's (or a query's) arguments come as the request's query: argued + * says so, and given is its text, kept on the request's work slot. */ +typedef struct { RouteKind kind; uint32_t target; size_t from, count; bool argued; const char *given; } Route; /* This node's key, for deliveries sealed to it (--key), and whether it * takes only those (--sealed-only), or only those from the senders named, @@ -223,6 +225,7 @@ struct Host { size_t views_capacity; uint64_t shown; /* views shown from snapshots */ Route *routes; /* by work slot */ + struct Given { char *text; size_t capacity; } *given; /* a view's arguments as asked, by work slot */ PureRuntime runtime; PureWork *work; Connection **table; /* open connections by index */ @@ -620,9 +623,10 @@ static const char *resolve(const Host *host, Connection *c, char *query, Route * uint64_t n; if (!value || (end && value > end)) return "query"; *value++ = 0; - bool from = !strcmp(pair, "from"); - if (!number(value, SIZE_MAX, &n) || (!from && strcmp(pair, "count"))) return "query"; - if (from) route->from = (size_t)n; + bool from = !strcmp(pair, "from"), paging = from || !strcmp(pair, "count"); + if (paging && !number(value, SIZE_MAX, &n)) return "query"; + if (!paging) route->argued = true; /* the query's view reads it, if it takes arguments */ + else if (from) route->from = (size_t)n; else route->count = (size_t)n; pair = end ? end + 1 : NULL; } @@ -631,7 +635,8 @@ static const char *resolve(const Host *host, Connection *c, char *query, Route * int found = route->kind == ROUTE_VIEW ? pure_module_view(m, c->key) : pure_module_input(m, c->key); /* An output is the app's to send and a timer ticks by itself: neither is posted. */ if (found >= 0 && route->kind == ROUTE_INPUT && - (m->input[found].kind == PURE_KIND_OUTPUT || m->input[found].kind == PURE_KIND_TIMER)) found = -1; + (m->input[found].kind == PURE_KIND_OUTPUT || m->input[found].kind == PURE_KIND_TIMER || + m->input[found].kind == PURE_KIND_ARGUMENTS)) found = -1; if (found < 0) return route->kind == ROUTE_VIEW ? "no_view" : "no_input"; *status = 400; route->target = (uint32_t)found; @@ -665,10 +670,10 @@ static const char *resolve(const Host *host, Connection *c, char *query, Route * *value++ = 0; if (route->kind == ROUTE_VIEW) { uint64_t n; - bool from = !strcmp(pair, "from"); - if (!number(value, SIZE_MAX, &n) || m->view_type[found] != PURE_VIEW_TABLE || (!from && strcmp(pair, "count"))) - return "query"; - if (from) route->from = (size_t)n; + bool from = !strcmp(pair, "from"), paging = from || !strcmp(pair, "count"); + if (!paging && m->view_arguments[found]) route->argued = true; /* an argument, read when shown */ + else if (!number(value, SIZE_MAX, &n) || m->view_type[found] != PURE_VIEW_TABLE || !paging) return "query"; + else if (from) route->from = (size_t)n; else route->count = (size_t)n; } else { uint32_t f = 0; @@ -818,6 +823,10 @@ static void start_request(Connection *c, size_t head_size) { memmove(c->in, c->in + head_size, extra); c->in_used = extra; if (!parse_head(c, head, head_size, &query)) { refuse(c, 400, "Bad Request", "request"); return; } + /* The query as it came: resolving takes it apart, and a view's arguments are read from it when shown. */ + char raw[HEADER_LIMIT]; + size_t raw_size = query ? strlen(query) : 0; + if (raw_size) memcpy(raw, query, raw_size + 1); /* A node that keeps its deliveries sealed refuses the others, and one * that names its senders refuses any other sender; a seal needs this * node's key, and binds the delivery's source, number and count. */ @@ -852,6 +861,17 @@ static void start_request(Connection *c, size_t head_size) { return; } host->by_slot[c->ticket.slot] = c; + if (route.argued) { + struct Given *given = &host->given[c->ticket.slot]; + if (given->capacity <= raw_size) { + char *grown = realloc(given->text, raw_size + 1); + if (!grown) abort(); + given->text = grown; + given->capacity = raw_size + 1; + } + memcpy(given->text, raw, raw_size + 1); + route.given = given->text; + } host->routes[c->ticket.slot] = route; if (!write) { c->phase = WAITING; return; } unsigned char *ingress = c->ingress = pure_runtime_ingress(c->ticket, NULL); /* NULL for no bytes */ @@ -1068,7 +1088,89 @@ static void put_number(Output *o, double value) { /* A view's value: a number in Pure's shortest form or a text, as text; a * table as {"rows":N,"from":F,"items":[{column: value}...]} for the rows * asked, each cell computed only for them (a cell without a value is null). */ +static int hex_value(char c) { + return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1; +} + +/* %XX decoded in place (never longer): the length after, or -1 for a bad escape. */ +static long percent_decoded(char *text, size_t length) { + size_t o = 0; + for (size_t i = 0; i < length; ++i) { + if (text[i] != '%') { text[o++] = text[i]; continue; } + int high = i + 2 < length ? hex_value(text[i + 1]) : -1, low = high >= 0 ? hex_value(text[i + 2]) : -1; + if (low < 0) return -1; + text[o++] = (char)(high << 4 | low); + i += 2; + } + return (long)o; +} + +/* A view's arguments from a query's name=value pairs (values %XX decoded; + * from and count page a table): a payload of their input's shape, the + * fixed fields in its prefix, the texts and bytes after it in order, each's + * length in the prefix but the last's. NULL for a pair naming no field or + * given twice, a fixed field missing or not of its type, or no memory. */ +static unsigned char *arguments_payload(const PureModule *m, int input, const char *given, size_t *size) { + const PureInputShape *shape = &m->input[input]; + size_t length = given ? strlen(given) : 0; + char *copy = malloc(length + 1); + unsigned char *payload = malloc(shape->prefix + length + 1); + const char *values[PURE_MODULE_FIELDS] = {0}; + long lengths[PURE_MODULE_FIELDS] = {0}; + bool ok = copy && payload; + if (ok) { + memcpy(copy, given ? given : "", length + 1); + memset(payload, 0, shape->prefix); + } + for (char *pair = copy; ok && pair && *pair;) { + char *end = strchr(pair, '&'), *value = strchr(pair, '='); + if (end) *end = 0; + if (!value || (end && value > end)) { ok = false; break; } + *value++ = 0; + uint32_t f = 0; + while (f < shape->field_count && strcmp(m->field[shape->field_start + f].name, pair)) ++f; + if (f == shape->field_count) ok = !strcmp(pair, "from") || !strcmp(pair, "count"); + else if (values[f]) ok = false; + else if ((lengths[f] = percent_decoded(value, strlen(value))) < 0) ok = false; + else values[f] = value; + pair = end ? end + 1 : NULL; + } + size_t at = shape->prefix; + for (uint32_t f = 0; ok && f < shape->field_count; ++f) { + const PureField *field = &m->field[shape->field_start + f]; + if (field->type == PURE_FIELD_U32 || field->type == PURE_FIELD_NUMBER) { + ok = values[f] && pure_json_fixed(field, (const unsigned char *)values[f], (size_t)lengths[f], payload); + continue; + } + if (field->offset != PURE_FIELD_REST) put_u32(payload + field->offset, (uint32_t)lengths[f]); + if (lengths[f]) memcpy(payload + at, values[f], (size_t)lengths[f]); + at += (size_t)lengths[f]; + } + free(copy); + if (!ok) { + free(payload); + return NULL; + } + *size = at; + return payload; +} + +static PureResult show_given(PureApp *app, const Route *route, size_t available, Applied *answer); + +/* A view shown with its arguments, when it takes some (they are the request's to give). */ static PureResult show_view(PureApp *app, const Route *route, size_t available, Applied *answer) { + int input = pure_module_arguments(app->module, route->target); + if (input < 0) return route->argued ? PURE_INVALID : show_given(app, route, available, answer); + size_t size; + unsigned char *payload = arguments_payload(app->module, input, route->given, &size); + if (!payload) return PURE_INVALID; + PureResult given = pure_app_arguments(app, route->target, (PureSpan){payload, size}, NULL); + PureResult status = given == PURE_OK ? show_given(app, route, available, answer) : PURE_INVALID; + free(payload); + return status; +} + +static PureResult show_given(PureApp *app, const Route *route, size_t available, Applied *answer) { const PureModule *m = app->module; uint32_t view = route->target; PureResource *out = &answer->resource; @@ -1952,7 +2054,8 @@ static bool start_shard(Host *host, const PureModule *module, bool documents, ui host->by_slot = calloc(host->capacity, sizeof(*host->by_slot)); host->jobs = calloc(host->capacity, sizeof(*host->jobs)); host->routes = calloc(host->capacity, sizeof(*host->routes)); - if (!host->work || !host->table || !host->by_slot || !host->jobs || !host->routes) { + host->given = calloc(host->capacity, sizeof(*host->given)); + if (!host->work || !host->table || !host->by_slot || !host->jobs || !host->routes || !host->given) { fputs("Cannot allocate connections\n", stderr); return false; } @@ -2058,6 +2161,8 @@ static void stop_shard(Host *host) { free(host->by_slot); free(host->jobs); free(host->routes); + for (size_t i = 0; host->given && i < host->capacity; ++i) free(host->given[i].text); + free(host->given); free(host->done); free(host->answering); free(host->views); diff --git a/tests/check.py b/tests/check.py index cc33947..2020363 100644 --- a/tests/check.py +++ b/tests/check.py @@ -1180,6 +1180,107 @@ def server_rows(aup, pure, work): 'orders_applied': applied, 'orders_refused': refused, 'logged': len(log)} +ARGUED = ''' +record Account { + aid = 0 + bid = 0 + balance = 0 + owner: text +} +state accounts: list of Account by aid +input open = message { + aid: u32 + bid: u32 + balance: number + owner: text +} +on open { accounts.add(Account { aid: open.aid, bid: open.bid, balance: open.balance, owner: open.owner }) } + +view balance(aid: u32) = accounts[aid].balance +view richer(low: number) = count(a in accounts where a.balance > low) +view hello(name: text, times: u32) = "{name} x{times}" +view branch(b: u32, over: number) = each a in accounts where a.bid == b && a.balance > over { + aid = a.aid + share = a.balance / sum(x.balance for x in accounts where x.bid == b) + owner = a.owner +} +view owned(who: text) = count(a in accounts where a.owner == who) +''' + + +def server_arguments(aup, pure, work): + """Views that take arguments, given as the request's query (values %XX + decoded): a keyed lookup, a count above an argument, texts shown back, + a table filtered by two arguments whose column folds with one of them, + paged; a registered query that takes one. Every answer against a model; + an argument missing, unknown or not of its type is 400 invalid.""" + import http.client + import random + import urllib.parse + source, module = work / 'argued.aup', work / 'argued.pmod' + source.write_text(ARGUED) + compile_module(aup, source, module) + host = HttpHost([pure / 'build/pure-serve', '--module', module, '--store', work / 'argued-store']) + + def call(method, path, body=None): + connection = http.client.HTTPConnection('127.0.0.1', host.port, timeout=30) + try: + connection.request(method, path, body, {'Content-Type': 'application/json'} if body is not None else {}) + answer = connection.getresponse() + return answer.status, answer.getheader('X-Pure-Error'), answer.read() + finally: + connection.close() + + rng = random.Random(5) + owners = ['김철수', 'bob', 'ann marie', 'a&b=c', '100%'] + accounts = {} + asked = 0 + try: + for aid in range(1, 201): + account = (aid % 7, rng.randrange(-500, 5000), rng.choice(owners)) + accounts[aid] = account + body = json.dumps({'aid': aid, 'bid': account[0], 'balance': account[1], 'owner': account[2]}).encode() + assert call('POST', '/inputs/open', body)[0] == 200 + for aid in rng.sample(range(1, 260), 60): + status, error, data = call('GET', f'/views/balance?aid={aid}') + assert (status, float(data)) == (200, accounts[aid][1]) if aid in accounts else (status, error) == (404, 'no_value') + asked += 1 + for low in [-1000, -1, 0, 0.5, 1000, 2500.25, 6000]: + assert float(call('GET', f'/views/richer?low={low}')[2]) == sum(1 for _, b, _ in accounts.values() if b > low) + asked += 1 + for name in ['세계 world', 'a&b=c', '100% sure', '😀']: + status, _, data = call('GET', f'/views/hello?name={urllib.parse.quote(name)}×=3') + assert (status, data.decode()) == (200, f'{name} x3') + asked += 1 + for who in owners: + data = call('GET', f'/views/owned?who={urllib.parse.quote(who)}')[2] + assert float(data) == sum(1 for _, _, o in accounts.values() if o == who) + asked += 1 + for b in range(7): + over = rng.choice([-1000, 0, 1500]) + rows = [(aid, balance, owner) for aid, (bid, balance, owner) in sorted(accounts.items()) if bid == b and balance > over] + total = sum(balance for bid, balance, _ in accounts.values() if bid == b) + page = json.loads(call('GET', f'/views/branch?b={b}&over={over}&from=2&count=5')[2]) + assert page['rows'] == len(rows) and page['from'] == min(2, len(rows)) + assert [(r['aid'], r['share'], r['owner']) for r in page['items']] == \ + [(aid, balance / total, owner) for aid, balance, owner in rows[2:7]] + asked += 1 + # Arguments missing, unknown, not of their type, or badly escaped. + for path in ['/views/balance', '/views/balance?aid=x', '/views/balance?aid=1&zz=2', '/views/balance?aid=-1', + '/views/hello?times=1&name=%zz', '/views/richer?low=1&low=2']: + assert call('GET', path)[:2] == (400, 'invalid'), path + assert call('GET', '/views/branch?b=1&from=x')[:2] == (400, 'query') + # A registered query takes arguments as a view does. + (work / 'top.aup').write_text('view top(k: u32) = each a in accounts where a.aid <= k {\n aid = a.aid\n}\n') + run(aup, 'query', source, work / 'top.aup', '-o', work / 'top.pmod') + assert call('PUT', '/queries/top', (work / 'top.pmod').read_bytes())[0] == 200 + assert [r['aid'] for r in json.loads(call('GET', '/queries/top?k=4')[2])['items']] == [1, 2, 3, 4] + assert call('GET', '/queries/top')[:2] == (400, 'invalid') + finally: + host.stop() + return {'accounts': len(accounts), 'asked': asked} + + def server_queries(aup, pure, work): """Queries as verified programs over a snapshot (PUT and GET /queries/NAME): aup query compiles a bank's declarations with views of its own; the node @@ -1766,6 +1867,7 @@ def main(): report['server_surface'] = server_surface(aup, pure, work) report['server_queries'] = server_queries(aup, pure, work) report['server_rows'] = server_rows(aup, pure, work) + report['server_arguments'] = server_arguments(aup, pure, work) report['server_answers'] = server_answers(aup, pure, work) print(json.dumps(report)) -- cgit v1.2.3