From 212bf53a0333e9df3ced0a3be79149eca43fe4ae Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 28 May 2026 10:09:33 +0000 Subject: [PATCH] =?UTF-8?q?fed-sx-m1:=20Step=208c-proj=20=E2=80=94=20GET?= =?UTF-8?q?=20/projections=20+=20/projections/{name}=20routes=20+=2011=20t?= =?UTF-8?q?ests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next/kernel/http_server.erl | 39 ++++++++++- next/tests/http_projections.sh | 118 +++++++++++++++++++++++++++++++++ plans/fed-sx-milestone-1.md | 3 +- 3 files changed, 156 insertions(+), 4 deletions(-) create mode 100755 next/tests/http_projections.sh diff --git a/next/kernel/http_server.erl b/next/kernel/http_server.erl index 943d47d2..fc2a8133 100644 --- a/next/kernel/http_server.erl +++ b/next/kernel/http_server.erl @@ -3,7 +3,9 @@ welcome_body/0, capabilities_body/0, capabilities_path/0, match_prefix/2, actors_prefix/0, actor_doc_response/1, - artifacts_prefix/0, artifact_response/1]). + artifacts_prefix/0, artifact_response/1, + projections_list_path/0, projections_prefix/0, + projections_list_response/0, projection_response/1]). %% HTTP request router per design §16.1. %% @@ -36,7 +38,11 @@ dispatch(<<71, 69, 84>>, <<47,46,119,101,108,108,45,107,110,111,119,110, 47,115,120,45,99,97,112,97,98,105,108,105,116,105,101,115>>) -> ok_response(capabilities_body()); -%% GET /actors/{id} or /artifacts/{cid} +%% GET /projections — list stub. Comes before the /projections/{name} +%% prefix clause because the bare path has no trailing slash. +dispatch(<<71, 69, 84>>, <<47,112,114,111,106,101,99,116,105,111,110,115>>) -> + projections_list_response(); +%% GET /actors/{id} or /artifacts/{cid} or /projections/{name} dispatch(<<71, 69, 84>>, Path) -> case match_prefix(actors_prefix(), Path) of {ok, Id} when byte_size(Id) > 0 -> @@ -46,7 +52,12 @@ dispatch(<<71, 69, 84>>, Path) -> {ok, Cid} when byte_size(Cid) > 0 -> artifact_response(Cid); _ -> - not_found_response() + case match_prefix(projections_prefix(), Path) of + {ok, Name} when byte_size(Name) > 0 -> + projection_response(Name); + _ -> + not_found_response() + end end end; dispatch(_, _) -> @@ -128,3 +139,25 @@ artifact_response(Cid) -> Pre = <<97,114,116,105,102,97,99,116,58,32>>, Body = <
>,
     ok_response(Body).
+
+%% "/projections" — 12 bytes (no trailing slash; the list endpoint)
+projections_list_path() ->
+    <<47,112,114,111,106,101,99,116,105,111,110,115>>.
+
+%% "/projections/" — 13 bytes (the per-projection prefix)
+projections_prefix() ->
+    <<47,112,114,111,106,101,99,116,105,111,110,115,47>>.
+
+%% Stub list response — real implementation queries the registry
+%% for active projections and serialises the name+CID list.
+projections_list_response() ->
+    %% "projections: (empty)\n" — hand-spelled
+    Body = <<112,114,111,106,101,99,116,105,111,110,115,58,32,
+             40,101,109,112,116,121,41,10>>,
+    ok_response(Body).
+
+projection_response(Name) ->
+    %% "projection: " — 12 bytes
+    Pre = <<112,114,111,106,101,99,116,105,111,110,58,32>>,
+    Body = <
>,
+    ok_response(Body).
diff --git a/next/tests/http_projections.sh b/next/tests/http_projections.sh
new file mode 100755
index 00000000..011764c8
--- /dev/null
+++ b/next/tests/http_projections.sh
@@ -0,0 +1,118 @@
+#!/usr/bin/env bash
+# next/tests/http_projections.sh — Step 8c-proj acceptance test.
+#
+# Exercises GET /projections (list stub) and GET /projections/{name}
+# via the shared match_prefix machinery. 11 cases.
+
+set -uo pipefail
+cd "$(git rev-parse --show-toplevel)"
+
+SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}"
+if [ ! -x "$SX_SERVER" ]; then
+  SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe"
+fi
+if [ ! -x "$SX_SERVER" ]; then
+  echo "ERROR: sx_server.exe not found." >&2
+  exit 1
+fi
+
+VERBOSE="${1:-}"
+PASS=0; FAIL=0; ERRORS=""
+TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT
+
+cat > "$TMPFILE" <<'EPOCHS'
+(epoch 1)
+(load "lib/erlang/tokenizer.sx")
+(load "lib/erlang/parser.sx")
+(load "lib/erlang/parser-core.sx")
+(load "lib/erlang/parser-expr.sx")
+(load "lib/erlang/parser-module.sx")
+(load "lib/erlang/transpile.sx")
+(load "lib/erlang/runtime.sx")
+(load "lib/erlang/vm/dispatcher.sx")
+
+(epoch 2)
+(eval "(get (erlang-load-module (file-read \"next/kernel/http_server.erl\")) :name)")
+
+;; projections_list_path is 12 bytes
+(epoch 10)
+(eval "(erlang-eval-ast \"byte_size(http_server:projections_list_path())\")")
+
+;; projections_prefix is 13 bytes (adds trailing slash)
+(epoch 11)
+(eval "(erlang-eval-ast \"byte_size(http_server:projections_prefix())\")")
+
+;; GET /projections -> 200 (list stub)
+(epoch 12)
+(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, http_server:projections_list_path()}], case http_server:route(Req) of [{status, 200} | _] -> ok; _ -> bad end\") :name)")
+
+;; List body has 'projections: ' prefix
+(epoch 13)
+(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, http_server:projections_list_path()}], R = http_server:route(Req), case R of [_, _, {body, B}] -> http_server:match_prefix(<<112,114,111,106,101,99,116,105,111,110,115,58,32>>, B) =/= nomatch; _ -> false end\") :name)")
+
+;; GET /projections/foo -> 200
+(epoch 14)
+(eval "(get (erlang-eval-ast \"Name = <<102,111,111>>, Req = [{method, <<71,69,84>>}, {path, <<(http_server:projections_prefix())/binary, Name/binary>>}], case http_server:route(Req) of [{status, 200} | _] -> ok; _ -> bad end\") :name)")
+
+;; Projection body has 'projection: ' prefix (singular)
+(epoch 15)
+(eval "(get (erlang-eval-ast \"Name = <<102,111,111>>, Req = [{method, <<71,69,84>>}, {path, <<(http_server:projections_prefix())/binary, Name/binary>>}], R = http_server:route(Req), case R of [_, _, {body, B}] -> http_server:match_prefix(<<112,114,111,106,101,99,116,105,111,110,58,32>>, B) =/= nomatch; _ -> false end\") :name)")
+
+;; GET /projections/ (empty name) -> 404
+(epoch 16)
+(eval "(get (erlang-eval-ast \"Req = [{method, <<71,69,84>>}, {path, http_server:projections_prefix()}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
+
+;; POST /projections -> 404
+(epoch 17)
+(eval "(get (erlang-eval-ast \"Req = [{method, <<80,79,83,84>>}, {path, http_server:projections_list_path()}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
+
+;; POST /projections/foo -> 404
+(epoch 18)
+(eval "(get (erlang-eval-ast \"Name = <<102,111,111>>, Req = [{method, <<80,79,83,84>>}, {path, <<(http_server:projections_prefix())/binary, Name/binary>>}], case http_server:route(Req) of [{status, 404} | _] -> ok; _ -> bad end\") :name)")
+
+;; No collision: actors / artifacts / projections all return 200 simultaneously
+(epoch 19)
+(eval "(get (erlang-eval-ast \"R1 = http_server:route([{method, <<71,69,84>>}, {path, <<47,97,99,116,111,114,115,47,97>>}]), R2 = http_server:route([{method, <<71,69,84>>}, {path, <<(http_server:artifacts_prefix())/binary, 98>>}]), R3 = http_server:route([{method, <<71,69,84>>}, {path, <<(http_server:projections_prefix())/binary, 99>>}]), case {R1, R2, R3} of {[{status, 200} | _], [{status, 200} | _], [{status, 200} | _]} -> ok; _ -> bad end\") :name)")
+EPOCHS
+
+OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
+
+check() {
+  local epoch="$1" desc="$2" expected="$3"
+  local actual
+  actual=$(echo "$OUTPUT" | awk -v e="$epoch" '
+    $0 ~ "^\\(ok-len " e " " { getline; print; exit }
+    $0 ~ "^\\(ok " e " "     { print; exit }
+    $0 ~ "^\\(error " e " "  { print; exit }
+  ')
+  [ -z "$actual" ] && actual=""
+  if echo "$actual" | grep -qF -- "$expected"; then
+    PASS=$((PASS+1))
+    [ "$VERBOSE" = "-v" ] && echo "  ok $desc"
+  else
+    FAIL=$((FAIL+1))
+    ERRORS+="  FAIL [$desc] (epoch $epoch) expected: $expected | actual: $actual
+"
+  fi
+}
+
+check  2  "module load name"                  "http_server"
+check 10  "projections_list_path = 12"        "12"
+check 11  "projections_prefix = 13"           "13"
+check 12  "GET /projections -> 200"           "ok"
+check 13  "list body 'projections: '"         "true"
+check 14  "GET /projections/foo -> 200"       "ok"
+check 15  "single body 'projection: '"        "true"
+check 16  "GET /projections/ -> 404"          "ok"
+check 17  "POST /projections -> 404"          "ok"
+check 18  "POST /projections/foo -> 404"      "ok"
+check 19  "all three /-routes 200"            "ok"
+
+TOTAL=$((PASS+FAIL))
+if [ $FAIL -eq 0 ]; then
+  echo "ok $PASS/$TOTAL next/tests/http_projections.sh passed"
+else
+  echo "FAIL $PASS/$TOTAL passed, $FAIL failed:"
+  echo "$ERRORS"
+fi
+[ $FAIL -eq 0 ]
diff --git a/plans/fed-sx-milestone-1.md b/plans/fed-sx-milestone-1.md
index 5a44afb5..025315e4 100644
--- a/plans/fed-sx-milestone-1.md
+++ b/plans/fed-sx-milestone-1.md
@@ -512,7 +512,7 @@ publish(ActorId, ActivityRequest) ->
 - [x] **8c-cap** — Route GET `/.well-known/sx-capabilities` (static doc: kernel/version/verbs lines). `next/tests/http_capabilities.sh` (8 cases). Other concrete routes follow.
 - [x] **8c-actors-doc** — `match_prefix/2` byte-level path-prefix matcher + GET `/actors/{id}` route returning an `actor: ` stub body. `/actors/{id}/outbox` deferred (needs path-segment splitting). `next/tests/http_actors.sh` (13 cases).
 - [x] **8c-art** — Route GET `/artifacts/{cid}` via `match_prefix`. Stub body echoes the cid (`artifact: \n`); real content store lookup deferred. `next/tests/http_artifacts.sh` (9 cases).
-- [ ] **8c-proj** — Routes `/projections` (list) + `/projections/{name}` (state).
+- [x] **8c-proj** — Routes GET `/projections` (list stub) + GET `/projections/{name}` (state stub) via `match_prefix`. Bare-path list endpoint dispatches before the prefix clause. `next/tests/http_projections.sh` (11 cases). Registry-backed implementation deferred.
 - [ ] **8c-post** — POST `/activity` glue: parse body → call `outbox:publish` with bearer-token auth (env var `NEXT_PUBLISH_TOKEN`).
 - [ ] **8d** — Content negotiation by Accept header: application/activity+json (default), application/cbor, application/json, application/sx.
 
@@ -988,6 +988,7 @@ A few things still under-specified; resolve as work begins.
 Newest first. One line per sub-deliverable commit. Erlang conformance gate
 (`bash lib/erlang/conformance.sh`) must remain 729/729 on every entry.
 
+- **2026-05-28** — Step 8c-proj: routes GET `/projections` (list stub returning `projections: (empty)\n`) + GET `/projections/{name}` (state stub returning `projection: \n`). Bare-path list clause dispatches before the prefix clause so `/projections` and `/projections/{name}` are distinguishable. All three dynamic-prefix routes (actors / artifacts / projections) compose cleanly — verified by a single combined-route test asserting all return 200 with distinct prefixes. Registry-backed implementation deferred — needs a running registry process at route time. `next/tests/http_projections.sh` 11/11. Erlang conformance 729/729.
 - **2026-05-28** — Step 8c-art: GET `/artifacts/{cid}` route added on top of `match_prefix`. Single GET dispatch clause now tries `actors_prefix` first, falls through to `artifacts_prefix` — no path collision (different leading bytes). Stub body echoes the CID with `artifact: ` prefix; real artifact-store lookup deferred to later (will key into the registry / genesis bundle). `next/tests/http_artifacts.sh` 9/9 covers happy path, empty-cid 404, POST 404, actor/artifact non-collision, static-route regression. Erlang conformance 729/729.
 - **2026-05-28** — Step 8c-actors-doc: `http_server` extended with `match_prefix/2` — pure byte-level prefix matcher built on Erlang binary pattern matching (`<>`-style head/tail walk). Empty prefix returns `{ok, FullPath}`; non-match returns `nomatch`; exact match returns `{ok, <<>>}`. Wired into a new GET `/actors/{id}` clause that extracts the id suffix and returns it as the body of `actor_doc_response/1` (stub: `actor: \n`). Empty id falls into 404. `/actors/{id}/outbox` deferred to a later step (needs segment splitting beyond prefix). `next/tests/http_actors.sh` 13/13. Erlang conformance 729/729.
 - **2026-05-28** — Step 8c-cap: GET `/.well-known/sx-capabilities` route + `capabilities_body/0` + `capabilities_path/0` exposed for tests. Body is a small plain-text descriptor with `kernel: fed-sx-m1`, `version: 0.0.1`, `verbs: Create Update Delete` (hand-spelled as integer-segment binary; string-literal segments unusable in this port). `next/tests/http_capabilities.sh` 8/8 covers method+path matching, body content, the existing GET / regression-free. Step 8c split into cap (done) + actors / art / proj / post — the rest need path-prefix matching helpers since `{id}` and `{cid}` are dynamic. Erlang conformance 729/729.