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>
343 lines
7.1 KiB
Plaintext
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
|