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>
This commit is contained in:
2026-05-27 10:54:12 +09:00
parent 4a959156ce
commit c72c2ff58d
5 changed files with 886 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
/*
* bridge_context.prg - FiveNode Request Context (Harbour side)
* JSON-based context access, output buffer wrappers
*
* Copyright (c) 2026 Charles KWON ( Charles KWON Ohjun, charleskwonohjun@gmail.com )
* All rights reserved.
*/
FUNCTION BRIDGE_SETUP_ERRORBLOCK()
LOCAL nH := FCreate( hb_DirTemp() + "errblock_proof.txt" )
FWrite( nH, "ErrorBlock SET at " + Time() )
FClose( nH )
ErrorBlock( {| e | Break( e ) } )
RETURN NIL
FUNCTION BRIDGE_PING()
hb_MemoWrit( hb_DirTemp() + "ping_proof.txt", "PING_EXECUTED" )
_BRIDGE_SET_RESULT( "PONG" )
RETURN NIL
FUNCTION hb_bridge_get_output()
RETURN _OUT_GET()
FUNCTION hb_bridge_clear_output()
_OUT_CLEAR()
RETURN NIL
FUNCTION bridge_output_append( cText )
_OUT_APPEND( cText )
RETURN NIL
// ── Context value accessor (extract from JSON via hb_jsonDecode) ──
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
FUNCTION ctx_set( cKey, xValue )
LOCAL cJson := _CTX_GET_JSON()
LOCAL hCtx
IF Empty( cJson )
hCtx := { => }
ELSE
hb_jsonDecode( cJson, @hCtx )
IF ValType( hCtx ) != "H"
hCtx := { => }
ENDIF
ENDIF
hCtx[ cKey ] := xValue
_CTX_SET_JSON( hb_jsonEncode( hCtx ) )
RETURN xValue

View File

@@ -0,0 +1,109 @@
/*
* bridge_cookie.prg - FiveNode
*
* Copyright (c) 2026 Charles KWON ( Charles KWON Ohjun, charleskwonohjun@gmail.com )
* All rights reserved.
*/
// bridge_cookie.prg
// Cookie functions (6) - mod_harbourv2 100% compatible
FUNCTION fn_GetCookies( cKey )
LOCAL hCookies := _parse_cookies()
IF cKey == NIL
RETURN hCookies
ENDIF
IF hb_HHasKey( hCookies, cKey )
RETURN hCookies[ cKey ]
ENDIF
RETURN ""
FUNCTION AP_COOKIE_READ( cName )
RETURN fn_GetCookies( cName )
FUNCTION fn_SetCookie( cName, cValue, nSecs, cPath, cDomain, lHttps, lOnlyHttp )
LOCAL cCookie, aCookies
hb_default( @cPath, "/" )
hb_default( @lHttps, .F. )
hb_default( @lOnlyHttp, .F. )
cCookie := cName + "=" + cValue
IF nSecs != NIL
cCookie += "; Max-Age=" + hb_ntos( nSecs )
ENDIF
cCookie += "; Path=" + cPath
IF cDomain != NIL
cCookie += "; Domain=" + cDomain
ENDIF
IF lHttps
cCookie += "; Secure"
ENDIF
IF lOnlyHttp
cCookie += "; HttpOnly"
ENDIF
aCookies := ctx_get( "cookies_out", {} )
AAdd( aCookies, cCookie )
ctx_set( "cookies_out", aCookies )
RETURN NIL
FUNCTION AP_COOKIE_WRITE( cName, cValue, nExpire )
RETURN fn_SetCookie( cName, cValue, nExpire )
FUNCTION AP_COOKIE_REMOVE( cName )
RETURN fn_SetCookie( cName, "", 0 )
FUNCTION AP_COOKIE_CHECK_STRING( cString )
IF Empty( cString )
RETURN .F.
ENDIF
IF ";" $ cString .OR. Chr(13) $ cString .OR. Chr(10) $ cString
RETURN .F.
ENDIF
RETURN .T.
// ── Cookie parsing helper ──
STATIC FUNCTION _parse_cookies()
LOCAL hHeaders, cRaw, aParts, cPart, aPair
LOCAL hCookies := { => }
hHeaders := AP_HEADERSIN()
IF hb_HHasKey( hHeaders, "Cookie" )
cRaw := hHeaders[ "Cookie" ]
aParts := hb_ATokens( cRaw, ";" )
FOR EACH cPart IN aParts
cPart := AllTrim( cPart )
aPair := hb_ATokens( cPart, "=" )
IF Len( aPair ) >= 2
hCookies[ AllTrim( aPair[1] ) ] := AllTrim( aPair[2] )
ENDIF
NEXT
ENDIF
RETURN hCookies
// -- mod_harbour compatibility wrappers --
FUNCTION MH_GetCookies( cKey )
RETURN fn_GetCookies( cKey )
FUNCTION MH_SetCookie( cName, cValue, nSecs, cPath, cDomain, lHttps, lOnlyHttp )
RETURN fn_SetCookie( cName, cValue, nSecs, cPath, cDomain, lHttps, lOnlyHttp )

View File

@@ -0,0 +1,342 @@
/*
* 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

View File

@@ -0,0 +1,266 @@
/*
* bridge_session.prg - FiveNode
*
* Copyright (c) 2026 Charles KWON ( Charles KWON Ohjun, charleskwonohjun@gmail.com )
* All rights reserved.
*/
#include "hbclass.ch"
// bridge_session.prg
// Session management - mod_harbourv2 MH_Sessions 100% compatible
THREAD STATIC ts_oSession
// ── Public API ──
FUNCTION fn_SessionInit( cName, nExpired )
hb_default( @cName, "MHSID" )
hb_default( @nExpired, 3600 )
ts_oSession := MH_Sessions():New( cName, nExpired )
ts_oSession:Init()
RETURN NIL
FUNCTION fn_Session( cKey, uValue )
IF ts_oSession == NIL
RETURN NIL
ENDIF
IF cKey == NIL
RETURN ts_oSession:hData
ENDIF
IF uValue != NIL
ts_oSession:hData[ cKey ] := uValue
RETURN uValue
ENDIF
IF hb_HHasKey( ts_oSession:hData, cKey )
RETURN ts_oSession:hData[ cKey ]
ENDIF
RETURN NIL
FUNCTION fn_SessionWrite()
IF ts_oSession != NIL
ts_oSession:Write()
ENDIF
RETURN NIL
FUNCTION fn_SessionEnd()
IF ts_oSession != NIL
ts_oSession:End()
ts_oSession := NIL
ENDIF
RETURN NIL
FUNCTION fn_SessionActive()
RETURN ts_oSession != NIL
FUNCTION fn_oSession( o )
IF o != NIL
ts_oSession := o
ENDIF
RETURN ts_oSession
FUNCTION fn_Garbage()
IF ts_oSession != NIL
ts_oSession:Garbage()
ENDIF
RETURN NIL
// ── MH_Sessions class ──
CREATE CLASS MH_Sessions
VAR cName
VAR cId
VAR nExpired
VAR hData
VAR cSessionPath
VAR lDestroy
METHOD New( cName, nExpired )
METHOD Init()
METHOD SetId()
METHOD SessionFile()
METHOD Read_CSID()
METHOD Read()
METHOD Write()
METHOD Data( cKey, uValue )
METHOD End()
METHOD Garbage( dMaxDate )
METHOD IsFile()
METHOD Info()
ENDCLASS
METHOD New( cName, nExpired ) CLASS MH_Sessions
::cName := cName
::nExpired := nExpired
::hData := { => }
::cId := ""
::lDestroy := .F.
::cSessionPath := ctx_get( "document_root", "." ) + "/.sessions"
IF ! hb_DirExists( ::cSessionPath )
hb_DirCreate( ::cSessionPath )
ENDIF
RETURN Self
METHOD Init() CLASS MH_Sessions
::Read_CSID()
IF Empty( ::cId )
::SetId()
fn_SetCookie( ::cName, ::cId, ::nExpired, "/", NIL, .F., .T. )
ELSE
::Read()
ENDIF
RETURN NIL
METHOD SetId() CLASS MH_Sessions
::cId := hb_MD5( hb_TToS( hb_DateTime() ) + hb_ntos( hb_RandomInt( 100000, 999999 ) ) )
RETURN ::cId
METHOD SessionFile() CLASS MH_Sessions
RETURN ::cSessionPath + "/" + ::cId + ".session"
METHOD Read_CSID() CLASS MH_Sessions
LOCAL cCookieVal := fn_GetCookies( ::cName )
IF ! Empty( cCookieVal )
::cId := cCookieVal
ENDIF
RETURN ::cId
METHOD Read() CLASS MH_Sessions
LOCAL cFile := ::SessionFile()
LOCAL cContent
IF hb_FileExists( cFile )
cContent := hb_MemoRead( cFile )
IF ! Empty( cContent )
hb_jsonDecode( cContent, @::hData )
IF ::hData == NIL
::hData := { => }
ENDIF
ENDIF
ENDIF
RETURN NIL
METHOD Write() CLASS MH_Sessions
LOCAL cFile
IF ::lDestroy
cFile := ::SessionFile()
IF hb_FileExists( cFile )
FErase( cFile )
ENDIF
RETURN NIL
ENDIF
cFile := ::SessionFile()
hb_MemoWrit( cFile, hb_jsonEncode( ::hData ) )
RETURN NIL
METHOD Data( cKey, uValue ) CLASS MH_Sessions
IF cKey == NIL
RETURN ::hData
ENDIF
IF uValue != NIL
::hData[ cKey ] := uValue
RETURN uValue
ENDIF
IF hb_HHasKey( ::hData, cKey )
RETURN ::hData[ cKey ]
ENDIF
RETURN NIL
METHOD End() CLASS MH_Sessions
::lDestroy := .T.
::Write()
fn_SetCookie( ::cName, "", 0, "/" )
RETURN NIL
METHOD Garbage( dMaxDate ) CLASS MH_Sessions
LOCAL aFiles, cFile
hb_default( @dMaxDate, Date() - 1 )
aFiles := Directory( ::cSessionPath + "/*.session" )
FOR EACH cFile IN aFiles
IF cFile[ 3 ] < dMaxDate
FErase( ::cSessionPath + "/" + cFile[ 1 ] )
ENDIF
NEXT
RETURN NIL
METHOD IsFile() CLASS MH_Sessions
RETURN hb_FileExists( ::SessionFile() )
METHOD Info() CLASS MH_Sessions
RETURN { ;
"id" => ::cId, ;
"name" => ::cName, ;
"expired" => ::nExpired, ;
"path" => ::cSessionPath, ;
"file" => ::SessionFile(), ;
"data" => ::hData, ;
"destroy" => ::lDestroy ;
}
// -- mod_harbour compatibility wrappers --
FUNCTION mh_SessionInit( cName, nExpired )
RETURN fn_SessionInit( cName, nExpired )
FUNCTION mh_Session( cKey, uValue )
RETURN fn_Session( cKey, uValue )
FUNCTION mh_SessionWrite()
RETURN fn_SessionWrite()
FUNCTION mh_SessionEnd()
RETURN fn_SessionEnd()
FUNCTION mh_SessionActive()
RETURN fn_SessionActive()
FUNCTION mh_oSession( o )
RETURN fn_oSession( o )
FUNCTION mh_Garbage()
RETURN fn_Garbage()

91
app/bridge_server.prg Normal file
View File

@@ -0,0 +1,91 @@
// app/bridge_server.prg — 1a.3-3 end-to-end glue.
//
// 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.
FUNCTION Main()
LOCAL cErr := HTTP_SERVER_START( ":8090", "BRIDGEDISPATCH" )
IF cErr != NIL
? "httpserver:", cErr
ENDIF
RETURN NIL
// Entry point invoked by httpserver. hReq comes from
// buildRequestHash in hbrtl_ext/httpserver/server.go.
FUNCTION BridgeDispatch( hReq )
LOCAL hCtx
// 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" ], ;
"query_string" => hReq[ "query" ], ;
"body" => hReq[ "body" ], ;
"remote_ip" => RemoteIPOnly( hReq[ "remote_addr" ] ), ;
"headers_in" => hReq[ "headers" ], ;
"headers_out" => { => }, ;
"status" => 200 ;
}
_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
ctx_set( "status", 404 )
AP_JSONRESPONSE( { "error" => "not found", "path" => hReq[ "path" ] } )
ENDCASE
// 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
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
// 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 )
RETURN ""
ENDIF
n := RAt( ":", cAddr )
IF n > 0
RETURN Left( cAddr, n - 1 )
ENDIF
RETURN cAddr