feat(dispatch): file-name routing via auto-renamed Main symbols (AOT)
Lets app/api/foo.prg keep its idiomatic `FUNCTION Main()` shape while
multiple such files compile into one binary. fnode auto-renames each
library file's Main into a unique symbol derived from the basename:
app/api/hello.prg -> HELLO__MAIN
app/api/admin-stats.prg -> ADMIN_STATS__MAIN (hyphen -> underscore)
Three moving parts:
cmd/fnode/main.go
parseOne for every PRG, then rename Main on every file except
the first (the entry). crossFile map updated so the analyzer
treats the renamed symbol as declared.
hbrtl_ext/dispatch/dispatch.go
New HB_FUNC FNODE_CALL(cFuncName) that does VM.FindSymbol +
PushSymbol/Function dance and discards the return value. Same
pattern pgserver's callPRG helper uses internally.
app/bridge_server.prg
BridgeDispatch now derives the symbol name from hReq["path"]
( /api/foo[.prg] -> FOO__MAIN ), invokes FNODE_CALL, and
maps "not found" errors to HTTP 404 (other errors -> 500).
Hardcoded /api/hello and /api/echo handlers replaced by the
path-driven model.
Verified end-to-end with app/api/hello.prg and app/api/admin-stats.prg:
GET /api/hello.prg -> 200 + JSON from HELLO__MAIN
GET /api/hello -> 200 (extension optional)
GET /api/admin-stats.prg?from=2026 -> 200 from ADMIN_STATS__MAIN
with query string echoed
GET /api/nope -> 404 "function not found"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
11
app/api/admin-stats.prg
Normal file
11
app/api/admin-stats.prg
Normal file
@@ -0,0 +1,11 @@
|
||||
// app/api/admin-stats.prg — renamed to ADMIN_STATS__MAIN. Verifies
|
||||
// hyphenated filenames map cleanly to underscore symbols.
|
||||
FUNCTION Main()
|
||||
AP_JSONRESPONSE( { ;
|
||||
"ok" => .t., ;
|
||||
"endpoint" => "admin-stats", ;
|
||||
"symbol" => "ADMIN_STATS__MAIN", ;
|
||||
"method" => AP_METHOD(), ;
|
||||
"query_string" => AP_ARGS() ;
|
||||
} )
|
||||
RETURN NIL
|
||||
10
app/api/hello.prg
Normal file
10
app/api/hello.prg
Normal file
@@ -0,0 +1,10 @@
|
||||
// app/api/hello.prg — fnode auto-renames this Main to HELLO__MAIN
|
||||
// so bridge_server.prg's PathToFunc("/api/hello.prg") finds it.
|
||||
FUNCTION Main()
|
||||
AP_JSONRESPONSE( { ;
|
||||
"ok" => .t., ;
|
||||
"msg" => "hello from api/hello.prg (auto-renamed Main)", ;
|
||||
"method" => AP_METHOD(), ;
|
||||
"ip" => AP_USERIP() ;
|
||||
} )
|
||||
RETURN NIL
|
||||
@@ -1,13 +1,10 @@
|
||||
// app/bridge_server.prg — 1a.3-3 end-to-end glue.
|
||||
// app/bridge_server.prg — file-name dispatcher.
|
||||
//
|
||||
// Wires httpserver (Go) → bridge_capi (Go) → bridge_*.prg so a PRG
|
||||
// handler written in the mod_harbour AP_* style runs inside the
|
||||
// fivenode_go single binary exactly as it would have run inside the
|
||||
// koffi/N-API fivenode bridge.
|
||||
//
|
||||
// Demo routing is hard-coded to two paths so we can curl the binary
|
||||
// and see the AP_* surface working end-to-end. File-name dispatch
|
||||
// (POST /api/foo.prg → app/api/foo.prg) is sub-phase 1a.4 work.
|
||||
// Translates the HTTP request hash into the ctx fields the mod_harbour
|
||||
// AP_* layer expects, then calls the per-path Main symbol that
|
||||
// fnode auto-renamed (app/api/foo.prg → FOO__MAIN). PRG handlers in
|
||||
// app/api/ stay unmodified — they keep their `FUNCTION Main()` shape
|
||||
// and use AP_METHOD/AP_BODY/AP_JSONRESPONSE/ctx_get/... freely.
|
||||
|
||||
FUNCTION Main()
|
||||
LOCAL cErr := HTTP_SERVER_START( ":8090", "BRIDGEDISPATCH" )
|
||||
@@ -16,13 +13,9 @@ FUNCTION Main()
|
||||
ENDIF
|
||||
RETURN NIL
|
||||
|
||||
// Entry point invoked by httpserver. hReq comes from
|
||||
// buildRequestHash in hbrtl_ext/httpserver/server.go.
|
||||
FUNCTION BridgeDispatch( hReq )
|
||||
LOCAL hCtx
|
||||
LOCAL hCtx, cFunc, cErr
|
||||
|
||||
// Translate the Go-side request hash into the ctx fields the
|
||||
// mod_harbour AP_* PRG layer expects to read.
|
||||
hCtx := { ;
|
||||
"method" => hReq[ "method" ], ;
|
||||
"filename" => hReq[ "path" ], ;
|
||||
@@ -36,49 +29,59 @@ FUNCTION BridgeDispatch( hReq )
|
||||
_CTX_SET_JSON( hb_jsonEncode( hCtx ) )
|
||||
_OUT_CLEAR()
|
||||
|
||||
// Hard-coded route dispatch. Replace with file-name dispatch in
|
||||
// 1a.4 once we want to land the labdb API surface unchanged.
|
||||
DO CASE
|
||||
CASE hReq[ "path" ] == "/api/hello"
|
||||
ApiHello()
|
||||
CASE hReq[ "path" ] == "/api/echo"
|
||||
ApiEcho()
|
||||
OTHERWISE
|
||||
cFunc := PathToFunc( hReq[ "path" ] )
|
||||
IF Empty( cFunc )
|
||||
ctx_set( "status", 404 )
|
||||
AP_JSONRESPONSE( { "error" => "not found", "path" => hReq[ "path" ] } )
|
||||
ENDCASE
|
||||
AP_JSONRESPONSE( { "error" => "no handler for path", "path" => hReq[ "path" ] } )
|
||||
ELSE
|
||||
cErr := FNODE_CALL( cFunc )
|
||||
IF ! Empty( cErr )
|
||||
_OUT_CLEAR()
|
||||
IF "not found" $ cErr
|
||||
ctx_set( "status", 404 )
|
||||
ELSE
|
||||
ctx_set( "status", 500 )
|
||||
ENDIF
|
||||
AP_JSONRESPONSE( { "error" => cErr, "func" => cFunc } )
|
||||
ENDIF
|
||||
ENDIF
|
||||
|
||||
// Assemble hResp from the buffered AP_* output.
|
||||
RETURN { ;
|
||||
"status" => ctx_get( "status", 200 ), ;
|
||||
"headers" => ctx_get( "headers_out", { => } ), ;
|
||||
"body" => _OUT_GET() ;
|
||||
}
|
||||
|
||||
FUNCTION ApiHello()
|
||||
AP_JSONRESPONSE( { ;
|
||||
"ok" => .t., ;
|
||||
"msg" => "hello from fivenode_go bridge layer", ;
|
||||
"method" => AP_METHOD(), ;
|
||||
"ip" => AP_USERIP() ;
|
||||
} )
|
||||
RETURN NIL
|
||||
// /api/admin-stats.prg -> ADMIN_STATS__MAIN
|
||||
// /api/hello.prg -> HELLO__MAIN
|
||||
// /api/hello -> HELLO__MAIN (extension optional)
|
||||
// Returns "" when the path can't be mapped (caller turns into 404).
|
||||
FUNCTION PathToFunc( cPath )
|
||||
LOCAL cBase, n
|
||||
IF SubStr( cPath, 1, 5 ) != "/api/"
|
||||
RETURN ""
|
||||
ENDIF
|
||||
cBase := SubStr( cPath, 6 )
|
||||
IF Right( cBase, 4 ) == ".prg"
|
||||
cBase := Left( cBase, Len( cBase ) - 4 )
|
||||
ENDIF
|
||||
IF Empty( cBase )
|
||||
RETURN ""
|
||||
ENDIF
|
||||
cBase := StrTran( cBase, "-", "_" )
|
||||
// Defensive: refuse anything outside [A-Z0-9_].
|
||||
FOR n := 1 TO Len( cBase )
|
||||
IF ! ( IsAlnum( SubStr( cBase, n, 1 ) ) .OR. SubStr( cBase, n, 1 ) == "_" )
|
||||
RETURN ""
|
||||
ENDIF
|
||||
NEXT
|
||||
RETURN Upper( cBase ) + "__MAIN"
|
||||
|
||||
FUNCTION ApiEcho()
|
||||
LOCAL cBody := AP_BODY()
|
||||
LOCAL hPayload := IIF( Empty( cBody ), { => }, hb_jsonDecode( cBody ) )
|
||||
AP_JSONRESPONSE( { ;
|
||||
"ok" => .t., ;
|
||||
"method" => AP_METHOD(), ;
|
||||
"query" => AP_ARGS(), ;
|
||||
"body_len" => Len( cBody ), ;
|
||||
"body_parsed" => hPayload, ;
|
||||
"user_agent" => hb_HGetDef( ctx_get( "headers_in", { => } ), "user-agent", "?" ) ;
|
||||
} )
|
||||
RETURN NIL
|
||||
FUNCTION IsAlnum( c )
|
||||
RETURN ( c >= "0" .AND. c <= "9" ) .OR. ;
|
||||
( c >= "A" .AND. c <= "Z" ) .OR. ;
|
||||
( c >= "a" .AND. c <= "z" )
|
||||
|
||||
// Strip the ":port" portion of a Go RemoteAddr so AP_USERIP returns
|
||||
// the bare IP the way mod_harbour does.
|
||||
FUNCTION RemoteIPOnly( cAddr )
|
||||
LOCAL n
|
||||
IF Empty( cAddr )
|
||||
|
||||
@@ -29,16 +29,3 @@ FUNCTION Main()
|
||||
? "result:", cResult
|
||||
|
||||
RETURN NIL
|
||||
|
||||
// Need ctx_get here because bridge_context.prg isn't linked in yet.
|
||||
FUNCTION ctx_get( cKey, xDefault )
|
||||
LOCAL cJson := _CTX_GET_JSON()
|
||||
LOCAL hCtx
|
||||
IF Empty( cJson )
|
||||
RETURN xDefault
|
||||
ENDIF
|
||||
hb_jsonDecode( cJson, @hCtx )
|
||||
IF ValType( hCtx ) == "H" .AND. hb_HHasKey( hCtx, cKey )
|
||||
RETURN hCtx[ cKey ]
|
||||
ENDIF
|
||||
RETURN xDefault
|
||||
|
||||
Reference in New Issue
Block a user