mirror of
https://github.com/oarkflow/mq.git
synced 2025-10-16 22:10:39 +08:00
update
This commit is contained in:
@@ -11,7 +11,7 @@ const (
|
|||||||
|
|
||||||
type NodeType int
|
type NodeType int
|
||||||
|
|
||||||
func (c NodeType) IsValid() bool { return c >= Function && c <= Page }
|
func (c NodeType) IsValid() bool { return c >= Function && c <= HTTPAPI }
|
||||||
|
|
||||||
func (c NodeType) String() string {
|
func (c NodeType) String() string {
|
||||||
switch c {
|
switch c {
|
||||||
@@ -19,6 +19,10 @@ func (c NodeType) String() string {
|
|||||||
return "Function"
|
return "Function"
|
||||||
case Page:
|
case Page:
|
||||||
return "Page"
|
return "Page"
|
||||||
|
case RPC:
|
||||||
|
return "RPC"
|
||||||
|
case HTTPAPI:
|
||||||
|
return "HTTPAPI"
|
||||||
}
|
}
|
||||||
return "Function"
|
return "Function"
|
||||||
}
|
}
|
||||||
@@ -26,6 +30,8 @@ func (c NodeType) String() string {
|
|||||||
const (
|
const (
|
||||||
Function NodeType = iota
|
Function NodeType = iota
|
||||||
Page
|
Page
|
||||||
|
RPC
|
||||||
|
HTTPAPI
|
||||||
)
|
)
|
||||||
|
|
||||||
type EdgeType int
|
type EdgeType int
|
||||||
|
114
examples/external_services.go
Normal file
114
examples/external_services.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/oarkflow/json"
|
||||||
|
"github.com/oarkflow/mq"
|
||||||
|
"github.com/oarkflow/mq/dag"
|
||||||
|
"github.com/oarkflow/mq/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Initialize handlers
|
||||||
|
handlers.Init()
|
||||||
|
|
||||||
|
// Create a DAG with external service integration
|
||||||
|
flow := dag.NewDAG("External Service Integration", "external-integration", func(taskID string, result mq.Result) {
|
||||||
|
fmt.Printf("DAG completed for task %s\n", taskID)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add RPC node for calling PHP Laravel RPC service
|
||||||
|
rpcHandler := handlers.NewRPCNodeHandler("laravel-rpc")
|
||||||
|
rpcHandler.SetConfig(dag.Payload{
|
||||||
|
Data: map[string]any{
|
||||||
|
"endpoint": "http://localhost:8000/api/rpc", // Laravel RPC endpoint
|
||||||
|
"method": "user.create", // RPC method name
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add HTTP API node for calling PHP Laravel REST API
|
||||||
|
httpHandler := handlers.NewHTTPAPINodeHandler("laravel-api")
|
||||||
|
httpHandler.SetConfig(dag.Payload{
|
||||||
|
Data: map[string]any{
|
||||||
|
"method": "POST",
|
||||||
|
"url": "http://localhost:8000/api/users", // Laravel API endpoint
|
||||||
|
"headers": map[string]any{
|
||||||
|
"Authorization": "Bearer your-token-here",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add nodes to DAG
|
||||||
|
flow.AddNode(dag.RPC, "Create User via RPC", "rpc-create-user", rpcHandler, true)
|
||||||
|
flow.AddNode(dag.HTTPAPI, "Create User via API", "api-create-user", httpHandler)
|
||||||
|
flow.AddNode(dag.Function, "Process Result", "process-result", &ProcessResult{})
|
||||||
|
|
||||||
|
// Add edges
|
||||||
|
flow.AddEdge(dag.Simple, "RPC to API", "rpc-create-user", "api-create-user")
|
||||||
|
flow.AddEdge(dag.Simple, "API to Process", "api-create-user", "process-result")
|
||||||
|
|
||||||
|
// Example payload
|
||||||
|
payload := map[string]any{
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"password": "securepassword",
|
||||||
|
"params": map[string]any{ // For RPC call
|
||||||
|
"user_data": map[string]any{
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set body for HTTP API call
|
||||||
|
payload["body"] = map[string]any{
|
||||||
|
"name": "John Doe",
|
||||||
|
"email": "john@example.com",
|
||||||
|
"password": "securepassword",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert payload to JSON bytes for processing
|
||||||
|
payloadBytes, err := json.Marshal(payload)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error marshaling payload: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the DAG
|
||||||
|
result := flow.Process(context.Background(), payloadBytes)
|
||||||
|
if result.Error != nil {
|
||||||
|
fmt.Printf("Error: %v\n", result.Error)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Success: %s\n", string(result.Payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessResult is a simple handler to process the final result
|
||||||
|
type ProcessResult struct {
|
||||||
|
dag.Operation
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ProcessResult) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||||
|
data, err := dag.UnmarshalPayload[map[string]any](ctx, task.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return mq.Result{Error: err, Ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Processing final result: %+v\n", data)
|
||||||
|
|
||||||
|
// Extract results from both RPC and HTTP API calls
|
||||||
|
rpcResult := data["rpc_response"]
|
||||||
|
apiResult := data["http_response"]
|
||||||
|
|
||||||
|
result := map[string]any{
|
||||||
|
"rpc_call_result": rpcResult,
|
||||||
|
"api_call_result": apiResult,
|
||||||
|
"processed": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, _ := json.Marshal(result)
|
||||||
|
return mq.Result{Payload: payload, Ctx: ctx}
|
||||||
|
}
|
165
handlers/http_api_handler.go
Normal file
165
handlers/http_api_handler.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oarkflow/json"
|
||||||
|
"github.com/oarkflow/mq"
|
||||||
|
"github.com/oarkflow/mq/dag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPAPINodeHandler handles HTTP API calls to external services
|
||||||
|
type HTTPAPINodeHandler struct {
|
||||||
|
dag.Operation
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPAPINodeHandler(id string) *HTTPAPINodeHandler {
|
||||||
|
handler := &HTTPAPINodeHandler{
|
||||||
|
Operation: dag.Operation{
|
||||||
|
ID: id,
|
||||||
|
Key: "http_api",
|
||||||
|
Type: dag.HTTPAPI,
|
||||||
|
Tags: []string{"external", "http", "api"},
|
||||||
|
},
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
handler.Payload = dag.Payload{Data: make(map[string]any)}
|
||||||
|
handler.RequiredFields = []string{"url"}
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTPAPINodeHandler) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||||
|
data, err := dag.UnmarshalPayload[map[string]any](ctx, task.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err), Ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract HTTP configuration from payload data
|
||||||
|
method, ok := h.Payload.Data["method"].(string)
|
||||||
|
if !ok {
|
||||||
|
method = "GET" // default to GET
|
||||||
|
}
|
||||||
|
|
||||||
|
url, ok := h.Payload.Data["url"].(string)
|
||||||
|
if !ok {
|
||||||
|
return mq.Result{Error: fmt.Errorf("HTTP URL not specified"), Ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare request body
|
||||||
|
var reqBody io.Reader
|
||||||
|
if method == "POST" || method == "PUT" || method == "PATCH" {
|
||||||
|
if body, exists := data["body"]; exists {
|
||||||
|
bodyBytes, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return mq.Result{Error: fmt.Errorf("failed to marshal request body: %w", err), Ctx: ctx}
|
||||||
|
}
|
||||||
|
reqBody = bytes.NewBuffer(bodyBytes)
|
||||||
|
} else if body, exists := h.Payload.Data["body"]; exists {
|
||||||
|
bodyBytes, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return mq.Result{Error: fmt.Errorf("failed to marshal request body: %w", err), Ctx: ctx}
|
||||||
|
}
|
||||||
|
reqBody = bytes.NewBuffer(bodyBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the HTTP call
|
||||||
|
result, err := h.makeHTTPCall(ctx, method, url, reqBody, data)
|
||||||
|
if err != nil {
|
||||||
|
return mq.Result{Error: fmt.Errorf("HTTP call failed: %w", err), Ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare response
|
||||||
|
responseData := map[string]any{
|
||||||
|
"http_response": result,
|
||||||
|
"original_data": data,
|
||||||
|
}
|
||||||
|
|
||||||
|
generated := h.GeneratedFields
|
||||||
|
if len(generated) == 0 {
|
||||||
|
generated = append(generated, h.ID)
|
||||||
|
}
|
||||||
|
for _, g := range generated {
|
||||||
|
data[g] = responseData
|
||||||
|
}
|
||||||
|
|
||||||
|
responsePayload, err := json.Marshal(responseData)
|
||||||
|
if err != nil {
|
||||||
|
return mq.Result{Error: fmt.Errorf("failed to marshal response: %w", err), Ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mq.Result{Payload: responsePayload, Ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTPAPINodeHandler) makeHTTPCall(ctx context.Context, method, url string, body io.Reader, data map[string]any) (map[string]any, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set headers
|
||||||
|
// req.Header.Set("Content-Type", "application/json")
|
||||||
|
// req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
// Add custom headers from configuration
|
||||||
|
if headers, ok := h.Payload.Data["headers"].(map[string]any); ok {
|
||||||
|
for k, v := range headers {
|
||||||
|
if strVal, ok := v.(string); ok {
|
||||||
|
req.Header.Set(k, strVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add query parameters from data
|
||||||
|
if queryParams, ok := data["query"].(map[string]any); ok {
|
||||||
|
q := req.URL.Query()
|
||||||
|
for k, v := range queryParams {
|
||||||
|
if strVal, ok := v.(string); ok {
|
||||||
|
q.Add(k, strVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := h.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("HTTP request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string]any{
|
||||||
|
"status_code": resp.StatusCode,
|
||||||
|
"status": resp.Status,
|
||||||
|
"headers": make(map[string]string),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert headers to map
|
||||||
|
for k, v := range resp.Header {
|
||||||
|
result["headers"].(map[string]string)[k] = strings.Join(v, ", ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse response body as JSON
|
||||||
|
var jsonBody interface{}
|
||||||
|
if err := json.Unmarshal(respBody, &jsonBody); err != nil {
|
||||||
|
// If not JSON, store as string
|
||||||
|
result["body"] = string(respBody)
|
||||||
|
} else {
|
||||||
|
result["body"] = jsonBody
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
@@ -23,4 +23,8 @@ func Init() {
|
|||||||
dag.AddHandler("output", func(id string) mq.Processor { return NewOutputHandler(id) })
|
dag.AddHandler("output", func(id string) mq.Processor { return NewOutputHandler(id) })
|
||||||
dag.AddHandler("split", func(id string) mq.Processor { return NewSplitHandler(id) })
|
dag.AddHandler("split", func(id string) mq.Processor { return NewSplitHandler(id) })
|
||||||
dag.AddHandler("join", func(id string) mq.Processor { return NewJoinHandler(id) })
|
dag.AddHandler("join", func(id string) mq.Processor { return NewJoinHandler(id) })
|
||||||
|
|
||||||
|
// External service handlers
|
||||||
|
dag.AddHandler("rpc", func(id string) mq.Processor { return NewRPCNodeHandler(id) })
|
||||||
|
dag.AddHandler("http_api", func(id string) mq.Processor { return NewHTTPAPINodeHandler(id) })
|
||||||
}
|
}
|
||||||
|
163
handlers/rpc_handler.go
Normal file
163
handlers/rpc_handler.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oarkflow/json"
|
||||||
|
"github.com/oarkflow/mq"
|
||||||
|
"github.com/oarkflow/mq/dag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RPCNodeHandler handles RPC calls to external services
|
||||||
|
type RPCNodeHandler struct {
|
||||||
|
dag.Operation
|
||||||
|
client *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCRequest represents a JSON-RPC 2.0 request
|
||||||
|
type RPCRequest struct {
|
||||||
|
Jsonrpc string `json:"jsonrpc"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params interface{} `json:"params,omitempty"`
|
||||||
|
ID interface{} `json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCResponse represents a JSON-RPC 2.0 response
|
||||||
|
type RPCResponse struct {
|
||||||
|
Jsonrpc string `json:"jsonrpc"`
|
||||||
|
Result interface{} `json:"result,omitempty"`
|
||||||
|
Error *RPCError `json:"error,omitempty"`
|
||||||
|
ID interface{} `json:"id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RPCError represents a JSON-RPC 2.0 error
|
||||||
|
type RPCError struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRPCNodeHandler(id string) *RPCNodeHandler {
|
||||||
|
handler := &RPCNodeHandler{
|
||||||
|
Operation: dag.Operation{
|
||||||
|
ID: id,
|
||||||
|
Key: "rpc",
|
||||||
|
Type: dag.RPC,
|
||||||
|
Tags: []string{"external", "rpc"},
|
||||||
|
},
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
handler.Payload = dag.Payload{Data: make(map[string]any)}
|
||||||
|
handler.RequiredFields = []string{"endpoint", "method"}
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RPCNodeHandler) ProcessTask(ctx context.Context, task *mq.Task) mq.Result {
|
||||||
|
data, err := dag.UnmarshalPayload[map[string]any](ctx, task.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return mq.Result{Error: fmt.Errorf("failed to unmarshal task payload: %w", err), Ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract RPC configuration from payload data
|
||||||
|
endpoint, ok := h.Payload.Data["endpoint"].(string)
|
||||||
|
if !ok {
|
||||||
|
return mq.Result{Error: fmt.Errorf("RPC endpoint not specified"), Ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
method, ok := h.Payload.Data["method"].(string)
|
||||||
|
if !ok {
|
||||||
|
return mq.Result{Error: fmt.Errorf("RPC method not specified"), Ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare RPC request
|
||||||
|
rpcReq := RPCRequest{
|
||||||
|
Jsonrpc: "2.0",
|
||||||
|
Method: method,
|
||||||
|
ID: task.ID, // Use task ID as request ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get parameters from task data or payload
|
||||||
|
if params, exists := data["params"]; exists {
|
||||||
|
rpcReq.Params = params
|
||||||
|
} else if params, exists := h.Payload.Data["params"]; exists {
|
||||||
|
rpcReq.Params = params
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any additional data from task payload as params if not already set
|
||||||
|
if rpcReq.Params == nil {
|
||||||
|
rpcReq.Params = data
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the RPC call
|
||||||
|
result, err := h.makeRPCCall(ctx, endpoint, rpcReq)
|
||||||
|
if err != nil {
|
||||||
|
return mq.Result{Error: fmt.Errorf("RPC call failed: %w", err), Ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare response
|
||||||
|
responseData := map[string]any{
|
||||||
|
"rpc_response": result,
|
||||||
|
"original_data": data,
|
||||||
|
}
|
||||||
|
generated := h.GeneratedFields
|
||||||
|
if len(generated) == 0 {
|
||||||
|
generated = append(generated, h.ID)
|
||||||
|
}
|
||||||
|
for _, g := range generated {
|
||||||
|
data[g] = responseData
|
||||||
|
}
|
||||||
|
responsePayload, err := json.Marshal(responseData)
|
||||||
|
if err != nil {
|
||||||
|
return mq.Result{Error: fmt.Errorf("failed to marshal response: %w", err), Ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mq.Result{Payload: responsePayload, Ctx: ctx}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *RPCNodeHandler) makeRPCCall(ctx context.Context, endpoint string, req RPCRequest) (interface{}, error) {
|
||||||
|
reqBody, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal RPC request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
httpReq.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
resp, err := h.client.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("HTTP request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var rpcResp RPCResponse
|
||||||
|
if err := json.Unmarshal(body, &rpcResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal RPC response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rpcResp.Error != nil {
|
||||||
|
return nil, fmt.Errorf("RPC error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rpcResp.Result, nil
|
||||||
|
}
|
Reference in New Issue
Block a user