Files
fivenode_go/app/bridge/bridge_request.prg
Charles KWON OhJun c72c2ff58d feat(bridge): port bridge_*.prg + wire HTTP↔bridge dispatcher
Pulls bridge_context.prg, bridge_request.prg, bridge_session.prg, and
bridge_cookie.prg from upstream fivenode into app/bridge/ so the
mod_harbour AP_* surface (AP_METHOD, AP_BODY, AP_ARGS, AP_USERIP,
AP_RPUTS, AP_JSONRESPONSE, AP_SETCONTENTTYPE, ctx_get, ctx_set, ...)
runs unchanged on top of fivenode_go's Go RTL.

bridge_main.prg deliberately omitted — its REQUEST sweep pulls in
TDrMySQL / hbct / hbcurl symbols fivenode_go neither has nor needs.
fivenode_go runs ahead-of-time with the Five compiler, so the
REQUEST trick that keeps fnb-runtime symbols alive isn't required.

One upstream patch was unavoidable: AP_RPUTS / AP_ECHO were variadic
(`( ... )`) and used PValue() to walk caller args. Five's PValue
returns the caller's LOCAL slot (not the actual variadic args, which
aren't copied into locals when declared params is 0), so the body
came back as "1" instead of the JSON payload. Collapsed both to a
single-argument form; every call site in fivenode_go already passes
exactly one value. Patched-out spots are marked with a TODO so we
can revert once Five gains real variadic PValue support.

app/bridge_server.prg ties it all together: starts httpserver on
:8090 with BRIDGEDISPATCH as the handler, hand-translates the Go
request hash into the ctx fields the AP_* layer reads, dispatches by
URL path (hard-coded /api/hello and /api/echo for now — file-name
dispatch lands in 1a.4), and assembles the response from the buffered
AP_* output + ctx_get("status") + ctx_get("headers_out").

Verified end-to-end:
  GET  /api/hello                       -> 200 JSON, method/ip echoed
  POST /api/echo?lang=ko (16-byte body) -> 200 JSON, body_parsed,
                                           query, user-agent
  GET  /api/nope                        -> 404 JSON

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 10:54:12 +09:00

343 lines
7.1 KiB
Plaintext

/*
* bridge_request.prg - FiveNode
*
* Copyright (c) 2026 Charles KWON ( Charles KWON Ohjun, charleskwonohjun@gmail.com )
* All rights reserved.
*/
// bridge_request.prg
// AP_* function reimplementation - 100% identical function names/signatures
// ── Request functions ──
FUNCTION AP_METHOD()
RETURN ctx_get( "method", "GET" )
FUNCTION AP_FILENAME()
RETURN ctx_get( "filename", "" )
FUNCTION AP_ARGS()
RETURN ctx_get( "query_string", "" )
FUNCTION AP_USERIP()
RETURN ctx_get( "remote_ip", "127.0.0.1" )
FUNCTION AP_BODY()
RETURN ctx_get( "body", "" )
FUNCTION AP_GETBODY()
RETURN ctx_get( "body", "" )
FUNCTION AP_GETENV( cVarName )
LOCAL hEnv := ctx_get( "env", { => } )
IF hb_HHasKey( hEnv, Upper( cVarName ) )
RETURN hEnv[ Upper( cVarName ) ]
ENDIF
RETURN hb_GetEnv( cVarName, "" )
// ── Headers In ──
FUNCTION AP_HEADERSIN()
RETURN ctx_get( "headers_in", { => } )
FUNCTION AP_HEADERSINCOUNT()
RETURN Len( ctx_get( "headers_in", { => } ) )
FUNCTION AP_HEADERSINKEY( n )
LOCAL hHeaders := ctx_get( "headers_in", { => } )
IF n >= 1 .AND. n <= Len( hHeaders )
RETURN hb_HKeyAt( hHeaders, n )
ENDIF
RETURN ""
FUNCTION AP_HEADERSINVAL( n )
LOCAL hHeaders := ctx_get( "headers_in", { => } )
IF n >= 1 .AND. n <= Len( hHeaders )
RETURN hb_HValueAt( hHeaders, n )
ENDIF
RETURN ""
// ── Headers Out ──
FUNCTION AP_HEADERSOUT()
RETURN ctx_get( "headers_out", { => } )
FUNCTION AP_HEADERSOUTCOUNT()
RETURN Len( ctx_get( "headers_out", { => } ) )
FUNCTION AP_HEADERSOUTKEY( n )
LOCAL hHeaders := ctx_get( "headers_out", { => } )
IF n >= 1 .AND. n <= Len( hHeaders )
RETURN hb_HKeyAt( hHeaders, n )
ENDIF
RETURN ""
FUNCTION AP_HEADERSOUTVAL( n )
LOCAL hHeaders := ctx_get( "headers_out", { => } )
IF n >= 1 .AND. n <= Len( hHeaders )
RETURN hb_HValueAt( hHeaders, n )
ENDIF
RETURN ""
FUNCTION AP_HEADERSOUTSET( cKey, cValue )
LOCAL hHeaders := ctx_get( "headers_out", { => } )
hHeaders[ cKey ] := cValue
ctx_set( "headers_out", hHeaders )
RETURN NIL
FUNCTION AP_SETCONTENTTYPE( cType )
RETURN AP_HEADERSOUTSET( "Content-Type", cType )
// ── Output ──
// fivenode_go patch: collapsed (...) -> single arg. Five's PValue returns
// the caller's LOCALs rather than the actual variadic args (declared
// params are 0 for `(...)`, so the runtime never copies args into the
// locals slot PValue reads). Every call site in this codebase passes
// exactly one value, so the collapse is behaviour-preserving.
// TODO: revert once Five gains true variadic PValue support.
FUNCTION AP_RPUTS( u )
IF ValType( u ) == "C"
bridge_output_append( u )
ELSE
bridge_output_append( fn_ValToChar( u ) )
ENDIF
RETURN NIL
FUNCTION AP_ECHO( u )
IF ValType( u ) == "C"
bridge_output_append( u )
ELSEIF ValType( u ) != "U"
bridge_output_append( fn_ValToChar( u ) )
ENDIF
RETURN NIL
// ── Parameter Parsing ──
FUNCTION AP_GetPairs( lUrlDecode )
LOCAL cArgs, aParams, aPair, hResult, cParam
hb_default( @lUrlDecode, .T. )
hResult := { => }
cArgs := AP_ARGS()
IF ! Empty( cArgs )
aParams := hb_ATokens( cArgs, "&" )
FOR EACH cParam IN aParams
aPair := hb_ATokens( cParam, "=" )
IF Len( aPair ) == 2
hResult[ aPair[ 1 ] ] := iif( lUrlDecode, hb_UrlDecode( aPair[ 2 ] ), aPair[ 2 ] )
ELSEIF Len( aPair ) == 1
hResult[ aPair[ 1 ] ] := ""
ENDIF
NEXT
ENDIF
RETURN hResult
FUNCTION AP_PostPairs( lUrlDecode )
LOCAL cBody, aParams, aPair, hResult, cParam
hb_default( @lUrlDecode, .T. )
hResult := { => }
cBody := AP_BODY()
IF ! Empty( cBody )
aParams := hb_ATokens( cBody, "&" )
FOR EACH cParam IN aParams
aPair := hb_ATokens( cParam, "=" )
IF Len( aPair ) == 2
hResult[ aPair[ 1 ] ] := iif( lUrlDecode, hb_UrlDecode( aPair[ 2 ] ), aPair[ 2 ] )
ELSEIF Len( aPair ) == 1
hResult[ aPair[ 1 ] ] := ""
ENDIF
NEXT
ENDIF
RETURN hResult
// ── URL decode ──
FUNCTION hb_UrlDecode( cString )
LOCAL cResult := "", i, c
cString := StrTran( cString, "+", " " )
i := 1
DO WHILE i <= Len( cString )
c := SubStr( cString, i, 1 )
IF c == "%" .AND. i + 2 <= Len( cString )
cResult += Chr( hb_HexToNum( SubStr( cString, i + 1, 2 ) ) )
i += 3
ELSE
cResult += c
i++
ENDIF
ENDDO
RETURN cResult
// ── Path/URL ──
FUNCTION fn_PathUrl()
RETURN ctx_get( "uri", "/" )
FUNCTION fn_PathBase( cDirFile )
LOCAL cRoot := ctx_get( "document_root", "." )
IF cDirFile != NIL
RETURN cRoot + "/" + cDirFile
ENDIF
RETURN cRoot
FUNCTION fn_Include( cFile )
RETURN hb_MemoRead( fn_PathBase( cFile ) )
FUNCTION fn_GetUri()
LOCAL cScheme := iif( ctx_get( "https", .F. ), "https", "http" )
LOCAL cHost := ""
LOCAL hHeaders := AP_HEADERSIN()
IF hb_HHasKey( hHeaders, "Host" )
cHost := hHeaders[ "Host" ]
ENDIF
RETURN cScheme + "://" + cHost + ctx_get( "uri", "/" )
FUNCTION fn_Redirect( cUrl )
AP_HEADERSOUTSET( "Location", cUrl )
ctx_set( "status", 302 )
RETURN NIL
// ── Data Conversion ──
FUNCTION fn_ValToChar( u )
LOCAL cType := ValType( u )
SWITCH cType
CASE "C" ; RETURN u
CASE "N" ; RETURN hb_ntos( u )
CASE "D" ; RETURN DToC( u )
CASE "L" ; RETURN iif( u, ".T.", ".F." )
CASE "U" ; RETURN "NIL"
CASE "A" ; RETURN hb_jsonEncode( u )
CASE "H" ; RETURN hb_jsonEncode( u )
CASE "T" ; RETURN hb_TToC( u )
ENDSWITCH
RETURN "(" + cType + ")"
// ctx_get / ctx_set are defined as PUBLIC in bridge_context.prg
// -- mod_harbour compatibility wrappers --
FUNCTION MH_PathUrl()
RETURN fn_PathUrl()
FUNCTION MH_PathBase( cDirFile )
RETURN fn_PathBase( cDirFile )
FUNCTION MH_Include( cFile )
RETURN fn_Include( cFile )
FUNCTION mh_GetUri()
RETURN fn_GetUri()
FUNCTION mh_Redirect( cUrl )
RETURN fn_Redirect( cUrl )
FUNCTION MH_ValToChar( u )
RETURN fn_ValToChar( u )
// ── JSON Response helpers ──
FUNCTION AP_JSONRESPONSE( xData, nStatus )
IF nStatus != NIL
ctx_set( "status", nStatus )
ENDIF
AP_SETCONTENTTYPE( "application/json" )
AP_RPUTS( hb_jsonEncode( xData ) )
RETURN NIL
FUNCTION AP_SETSTATUS( nStatus )
ctx_set( "status", nStatus )
RETURN NIL
// ── Auth (read-only from .prg, set by Node.js only) ──
FUNCTION fn_Auth()
LOCAL cAuth := _AUTH_GET()
LOCAL hAuth
IF Empty( cAuth )
RETURN { "authenticated" => .F. }
ENDIF
hb_jsonDecode( cAuth, @hAuth )
IF ValType( hAuth ) != "H"
RETURN { "authenticated" => .F. }
ENDIF
RETURN hAuth
FUNCTION fn_IsAuth()
RETURN fn_HGetDef( fn_Auth(), "authenticated", .F. )
FUNCTION fn_AuthUser()
RETURN fn_HGetDef( fn_Auth(), "user", "" )
FUNCTION fn_AuthRole()
RETURN fn_HGetDef( fn_Auth(), "role", "" )
// ── Hash helper ──
FUNCTION fn_HGetDef( hHash, cKey, xDefault )
RETURN hb_HGetDef( hHash, cKey, xDefault )
// ── JSON Body helper ──
FUNCTION AP_JSONBODY()
LOCAL cBody := AP_BODY()
LOCAL xResult
IF ! Empty( cBody )
hb_jsonDecode( cBody, @xResult )
ENDIF
RETURN xResult