├── .gitignore ├── LICENSE ├── README.md ├── README_ZH.md ├── action ├── README.md └── README_ZH.md ├── endpoint ├── README.md ├── README_ZH.md ├── beanstalkd │ └── benstalkd.go ├── grpc_stream │ ├── grpc_stream.go │ └── testdata │ │ ├── Makefile │ │ ├── api │ │ └── ble │ │ │ └── v1 │ │ │ └── ble.proto │ │ ├── go.mod │ │ └── main.go ├── kafka │ ├── kafka.go │ └── kafka_test.go ├── nats │ ├── nats.go │ └── nats_test.go ├── rabbitmq │ ├── rabbitmq.go │ └── rabbitmq_test.go ├── redis │ ├── redis.go │ └── redis_test.go ├── redis_stream │ ├── redis.go │ └── redis_test.go └── wukongim │ └── wukongim.go ├── examples ├── kafka │ └── kafka_producer.go ├── redis │ └── call_redis_client.go └── wukongim │ └── wukongim_sender.go ├── external ├── README.md ├── README_ZH.md ├── beanstalkd │ ├── tube_node.go │ └── worker_node.go ├── grpc │ ├── grpc_client.go │ ├── grpc_client_test.go │ └── testdata │ │ ├── helloworld │ │ ├── helloworld.pb.go │ │ ├── helloworld.proto │ │ └── helloworld_grpc.pb.go │ │ └── server.go ├── kafka │ ├── kafka_producer.go │ └── kafka_producer_test.go ├── mongodb │ └── mongodb_client.go ├── nats │ ├── nats_client.go │ └── nats_client_test.go ├── opengemini │ ├── query.go │ ├── query_test.go │ ├── utils.go │ ├── utils_test.go │ ├── write.go │ └── write_test.go ├── otel │ └── otel_client.go ├── rabbitmq │ ├── rabbitmq_client.go │ └── rabbitmq_client_test.go ├── redis │ ├── redis_client.go │ ├── redis_client_test.go │ ├── redis_publisher.go │ └── redis_publisher_test.go └── wukongim │ └── wukongim_sender.go ├── filter ├── README.md ├── README_ZH.md ├── lua_filter.go ├── lua_filter_test.go └── testdata │ └── script.lua ├── go.mod ├── go.sum ├── pkg └── lua_engine │ ├── engine.go │ ├── engine_test.go │ └── tools.go ├── stats ├── README.md └── README_ZH.md ├── testdata ├── chain_call_rest_api.json ├── chain_msg_type_switch.json ├── filter_node.json ├── not_debug_mode_chain.json └── test_context_chain.json └── transform ├── README.md ├── README_ZH.md ├── lua_transform.go ├── lua_transform_test.go └── testdata ├── libs_script.lua └── script.lua /.gitignore: -------------------------------------------------------------------------------- 1 | # JetBrains template 2 | .idea 3 | *.iml 4 | out/ 5 | 6 | # File-based project format 7 | *.iws 8 | 9 | # JIRA plugin 10 | atlassian-ide-plugin.xml 11 | 12 | ### Go template 13 | # Binaries for programs and plugins 14 | *.exe 15 | *.exe~ 16 | *.dll 17 | *.so 18 | *.dylib 19 | 20 | # Test binary, built with `go test -c` 21 | *.test 22 | 23 | # Output of the go coverage tool, specifically when used with LiteIDE 24 | *.out 25 | 26 | # Dependency directories (remove the comment below to include it) 27 | vendor/ 28 | 29 | ### macOS template 30 | .DS_Store 31 | .AppleDouble 32 | .LSOverride 33 | 34 | # Thumbnails 35 | ._* 36 | 37 | # VSCode 38 | .vscode 39 | 40 | # log 41 | *.log 42 | 43 | # coverage file 44 | coverage.html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rulego-components 2 | 3 | `rulego-components` is a rule engine extension component library for [RuleGo](https://github.com/rulego/rulego). 4 | 5 | ## Features 6 | The component library is divided into the following submodules: 7 | * **endpoint:** Receiver endpoint, responsible for listening and receiving data, and then handing it over to the `RuleGo` rule engine for processing. For example: MQTT endpoint (subscribe to MQTT Broker data), HTTP endpoint (HTTP server) 8 | * **filter:** Filter the messages. 9 | - [x/luaFilter](/filter/lua_filter.go) Use lua script to filter the messages. 10 | * **transform:** Transform the messages. 11 | - [x/luaTransform](/transform/lua_transform.go) Use lua script to transform the messages. 12 | * **action:** Perform some actions. 13 | * **stats:** Perform statistics and analysis on the data. 14 | * **external:** External integration, integrate with third-party systems, such as: calling kafka, database, third-party api, etc. 15 | - [x/kafkaProducer](/external/kafka/kafka_producer.go) kafka producer. 16 | - [x/redisClient](/external/redis/redis_client.go) redis client. 17 | 18 | ## Installation 19 | 20 | Use the `go get` command to install `rulego-components`: 21 | 22 | ```bash 23 | go get github.com/rulego/rulego-components 24 | ``` 25 | 26 | ## Usage 27 | 28 | Use the blank identifier to import the extension component, and the extension component will automatically register to `RuleGo` 29 | ```go 30 | _ "github.com/rulego/rulego-components/external/redis" 31 | ``` 32 | 33 | Then use the type specified by the component in the rule chain JSON file to call the extension component 34 | ```json 35 | { 36 | "ruleChain": { 37 | "id": "rule01", 38 | "name": "Test rule chain" 39 | }, 40 | "metadata": { 41 | "nodes": [ 42 | { 43 | "id": "s1", 44 | "type": "x/redisClient", 45 | "name": "Name", 46 | "debugMode": true, 47 | "configuration": { 48 | "field1": "Configuration parameters defined by the component", 49 | "....": "..." 50 | } 51 | } 52 | ], 53 | "connections": [ 54 | { 55 | "fromId": "s1", 56 | "toId": "Connect to the next component ID", 57 | "type": "The connection relationship with the component" 58 | } 59 | ] 60 | } 61 | } 62 | ``` 63 | 64 | ## Contributing 65 | 66 | The core feature of `RuleGo` is componentization, where all business logic is composed of components, and they can be flexibly configured and reused. Currently, `RuleGo` has built-in some common components, such as message type Switch, JavaScript Switch, JavaScript Filter, JavaScript Transformer, HTTP Push, MQTT Push, Send Email, Log Record and so on. 67 | However, we know that these components are far from meeting the needs of all users, so we hope to have more developers contribute to RuleGo's extension components, making RuleGo's ecosystem more rich and powerful. 68 | 69 | If you are interested in RuleGo and want to contribute to its extension components, you can follow these steps: 70 | 71 | - Read RuleGo's [documentation](https://rulego.cc) , and learn about its architecture, features and usage. 72 | - Fork RuleGo's [repository](https://github.com/rulego/rulego) , and clone it to your local machine. 73 | - Refer to RuleGo's [examples](https://github.com/rulego/rulego/tree/main/components) , and write your own extension component, implementing the corresponding interfaces and methods. 74 | - Test your extension component locally, and make sure it works properly and correctly. 75 | - Submit your code and create a pull request, we will review and merge your contribution as soon as possible. 76 | - Give a star to the RuleGo project on GitHub/Gitee, and let more people know about it. 77 | 78 | We welcome and appreciate any form of contribution, whether it is code, documentation, suggestion or feedback. We believe that with your support, RuleGo will become a better rule engine and event processing framework. Thank you! 79 | 80 | If the component code you submit has no third-party dependencies or is a general-purpose component, please submit it to the built-in `components` under [github.com/rulego/rulego](https://github.com/rulego/rulego) , otherwise submit it to this repository: [rulego-components](https://github.com/rulego/rulego-components) . 81 | 82 | ## License 83 | 84 | `RuleGo` is licensed under the Apache 2.0 License - see the [LICENSE] file for details. 85 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | # rulego-components 2 | 3 | `rulego-components` 是 [RuleGo](https://github.com/rulego/rulego) 规则引擎扩展组件库。 4 | 5 | ## 特性 6 | 组件库分为以下子模块: 7 | * **endpoint:** 接收端端点,负责监听并接收数据,然后交给`RuleGo`规则引擎处理。例如:MQTT Endpoint(订阅MQTT Broker 数据)、REST Endpoint(HTTP Server)、Websocket Endpoint、Kafka Endpoint 8 | * **filter:** 对消息进行过滤。 9 | - [x/luaFilter](/filter/lua_filter.go) 使用lua脚本对消息进行过滤。 10 | * **transform:** 对消息进行转换。 11 | - [x/luaTransform](/transform/lua_transform.go) 使用lua脚本对消息进行转换。 12 | * **action:** 执行某些动作。 13 | * **stats:** 对数据进行统计、分析。 14 | * **external:** 外部集成,和第三方系统进行集成,例如:调用kafka、数据库、第三方api等。 15 | - [x/kafkaProducer](/external/kafka/kafka_producer.go) kafka 生产者。 16 | - [x/redisClient](/external/redis/redis_client.go) redis 客户端。 17 | 18 | ## 安装 19 | 20 | 使用`go get`命令安装`rulego-components`: 21 | 22 | ```bash 23 | go get github.com/rulego/rulego-components 24 | ``` 25 | 26 | ## 使用 27 | 28 | 使用空白标识符导入扩展组件,扩展组件会自动注册到`RuleGo` 29 | ```go 30 | _ "github.com/rulego/rulego-components/external/redis" 31 | ``` 32 | 33 | 然后在规则链JSON文件使用组件指定的type调用扩展组件 34 | ```json 35 | { 36 | "ruleChain": { 37 | "id": "rule01", 38 | "name": "测试规则链" 39 | }, 40 | "metadata": { 41 | "nodes": [ 42 | { 43 | "id": "s1", 44 | "type": "x/redisClient", 45 | "name": "名称", 46 | "debugMode": true, 47 | "configuration": { 48 | "field1": "组件定义的配置参数", 49 | "....": "..." 50 | } 51 | } 52 | ], 53 | "connections": [ 54 | { 55 | "fromId": "s1", 56 | "toId": "连接下一个组件ID", 57 | "type": "与组件的连接关系" 58 | } 59 | ] 60 | } 61 | } 62 | ``` 63 | 64 | ## 贡献 65 | 66 | `RuleGo` 的核心特性是组件化,所有业务逻辑都是组件,并能灵活配置和重用它们。目前,`RuleGo` 已经内置了一些常用的组件,如消息类型 Switch, JavaScript Switch, JavaScript 过滤器, JavaScript 转换器, HTTP 推送, MQTT 推送, 发送邮件, 日志记录 等组件。 67 | 但是,我们知道这些组件还远远不能满足所有用户的需求,所以我们希望能有更多的开发者为 RuleGo 贡献扩展组件,让 RuleGo 的生态更加丰富和强大。 68 | 69 | 如果你对 RuleGo 感兴趣,并且想要为它贡献扩展组件,你可以参考以下步骤: 70 | 71 | - 阅读 RuleGo 的 [文档](https://rulego.cc) ,了解其架构、特性和使用方法。 72 | - Fork RuleGo 的 [仓库](https://github.com/rulego/rulego) ,并 clone 到本地。 73 | - 参考 RuleGo 的 [示例](https://github.com/rulego/rulego/tree/main/components) ,编写你自己的扩展组件,并实现相应的接口和方法。 74 | - 在本地测试你的扩展组件,确保其功能正常且无误。 75 | - 提交你的代码,并发起 pull request,我们会尽快审核并合并你的贡献。 76 | - 在 GitHub/Gitee 上给 RuleGo 项目点个星星,让更多的人知道它。 77 | 78 | 我们非常欢迎和感谢任何形式的贡献,无论是代码、文档、建议还是反馈。我们相信,有了你们的支持,RuleGo 会成为一个更好的规则引擎和事件处理框架。谢谢! 79 | 80 | 如果您提交的组件代码没有第三方依赖或者是通用性组件请提交到:[github.com/rulego/rulego](https://github.com/rulego/rulego) 下的内置`components`, 81 | 否则提交到本仓库:[rulego-components](https://github.com/rulego/rulego-components) 。 82 | 83 | ## 交流群 84 | 85 | QQ群号:**720103251** 86 | 87 | 88 | ## 许可 89 | 90 | `RuleGo`使用Apache 2.0许可证,详情请参见[LICENSE](LICENSE)文件。 -------------------------------------------------------------------------------- /action/README.md: -------------------------------------------------------------------------------- 1 | # action 2 | Perform some actions. 3 | 4 | ## How to customize components 5 | Implement the `types.Node` interface. 6 | Example of a custom component: 7 | ```go 8 | // Define the Node component 9 | // UpperNode A plugin that converts the message data to uppercase 10 | type UpperNode struct{} 11 | 12 | func (n *UpperNode) Type() string { 13 | return "test/upper" 14 | } 15 | 16 | func (n *UpperNode) New() types.Node { 17 | return &UpperNode{} 18 | } 19 | 20 | func (n *UpperNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 21 | // Do some initialization work 22 | return nil 23 | } 24 | 25 | // Process the message 26 | func (n *UpperNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 27 | msg.Data = strings.ToUpper(msg.Data) 28 | // Send the modified message to the next node 29 | ctx.TellSuccess(msg) 30 | return nil 31 | } 32 | 33 | func (n *UpperNode) Destroy() { 34 | // Do some cleanup work 35 | } 36 | ``` 37 | 38 | ## Usage 39 | 40 | Register the component to the `RuleGo` default registry: 41 | ```go 42 | rulego.Registry.Register(&MyNode{}) 43 | ``` 44 | 45 | Then use your component in the rule chain DSL file: 46 | ```json 47 | { 48 | "ruleChain": { 49 | "id": "rue01", 50 | "name": "Test rule chain" 51 | }, 52 | "metadata": { 53 | "nodes": [ 54 | { 55 | "id": "s1", 56 | "type": "test/upper", 57 | "name": "Name", 58 | "debugMode": true, 59 | "configuration": { 60 | "field1": "Configuration parameters defined by the component", 61 | "....": "..." 62 | } 63 | } 64 | ], 65 | "connections": [ 66 | { 67 | "fromId": "s1", 68 | "toId": "Connect to the next component ID", 69 | "type": "The connection relationship with the component" 70 | } 71 | ] 72 | } 73 | } 74 | ``` -------------------------------------------------------------------------------- /action/README_ZH.md: -------------------------------------------------------------------------------- 1 | # action 2 | 3 | 执行某些动作。 4 | 5 | ## 怎样自定义组件 6 | 实现`types.Node`接口,例子: 7 | ```go 8 | //定义Node组件 9 | //UpperNode A plugin that converts the message data to uppercase 10 | type UpperNode struct{} 11 | 12 | func (n *UpperNode) Type() string { 13 | return "test/upper" 14 | } 15 | func (n *UpperNode) New() types.Node { 16 | return &UpperNode{} 17 | } 18 | func (n *UpperNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 19 | // Do some initialization work 20 | return nil 21 | } 22 | //处理消息 23 | func (n *UpperNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 24 | msg.Data = strings.ToUpper(msg.Data) 25 | // Send the modified message to the next node 26 | ctx.TellSuccess(msg) 27 | return nil 28 | } 29 | 30 | func (n *UpperNode) Destroy() { 31 | // Do some cleanup work 32 | } 33 | ``` 34 | 35 | ## 使用 36 | 37 | 把组件注册到`RuleGo`默认注册器 38 | ```go 39 | rulego.Registry.Register(&MyNode{}) 40 | ``` 41 | 42 | 然后在规则链DSL文件使用您的组件 43 | ```json 44 | { 45 | "ruleChain": { 46 | "id": "rue01", 47 | "name": "测试规则链" 48 | }, 49 | "metadata": { 50 | "nodes": [ 51 | { 52 | "id": "s1", 53 | "type": "test/upper", 54 | "name": "名称", 55 | "debugMode": true, 56 | "configuration": { 57 | "field1": "组件定义的配置参数", 58 | "....": "..." 59 | } 60 | } 61 | ], 62 | "connections": [ 63 | { 64 | "fromId": "s1", 65 | "toId": "连接下一个组件ID", 66 | "type": "与组件的连接关系" 67 | } 68 | ] 69 | } 70 | } 71 | ``` -------------------------------------------------------------------------------- /endpoint/README.md: -------------------------------------------------------------------------------- 1 | # endpoint 2 | Receiver endpoint, responsible for listening and receiving data, and then handing it over to the `RuleGo` rule engine for processing. For example: MQTT endpoint (subscribe to MQTT Broker data), HTTP endpoint (HTTP server) 3 | 4 | ## How to customize endpoints 5 | TODO -------------------------------------------------------------------------------- /endpoint/README_ZH.md: -------------------------------------------------------------------------------- 1 | # endpoint 2 | 接收端端点,负责监听并接收数据,然后交给`RuleGo`规则引擎处理。例如:MQTT endpoint(订阅MQTT Broker 数据)、HTTP endpoint(HTTP server) 3 | 4 | ## 怎样endpoint组件 5 | TODO -------------------------------------------------------------------------------- /endpoint/grpc_stream/testdata/Makefile: -------------------------------------------------------------------------------- 1 | PROTO_FILE = ./api/ble/v1/ble.proto 2 | GO_OUT_DIR = . 3 | BINARY_NAME = ble_server 4 | MAIN_FILE = main.go 5 | 6 | .PHONY: all proto build clean 7 | 8 | all: proto build 9 | 10 | proto: 11 | @echo "Generating protobuf code..." 12 | protoc --go_out=$(GO_OUT_DIR) \ 13 | --go_opt=paths=source_relative \ 14 | --go-grpc_out=$(GO_OUT_DIR) \ 15 | --go-grpc_opt=paths=source_relative \ 16 | $(PROTO_FILE) 17 | @echo "Proto generation completed" 18 | 19 | build: 20 | @echo "Building binary..." 21 | go build -o $(BINARY_NAME) $(MAIN_FILE) 22 | @echo "Build completed" 23 | 24 | # 直接运行 25 | run: proto build 26 | @echo "Starting server..." 27 | ./$(BINARY_NAME) 28 | 29 | clean: 30 | @echo "Cleaning..." 31 | rm -f $(BINARY_NAME) 32 | @echo "Clean completed" -------------------------------------------------------------------------------- /endpoint/grpc_stream/testdata/api/ble/v1/ble.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package ble; 4 | 5 | option go_package = "github.com/rulego/rulego/endpoint/ble"; 6 | 7 | service DataService { 8 | rpc StreamData(StreamRequest) returns (stream DataResponse); 9 | } 10 | 11 | message StreamRequest { 12 | string client_id = 1; 13 | } 14 | 15 | message DataResponse { 16 | string type = 1; 17 | bytes payload = 2; 18 | int64 timestamp = 3; 19 | } -------------------------------------------------------------------------------- /endpoint/grpc_stream/testdata/go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | google.golang.org/grpc v1.69.2 7 | google.golang.org/protobuf v1.36.0 8 | ) 9 | 10 | require ( 11 | golang.org/x/net v0.30.0 // indirect 12 | golang.org/x/sys v0.26.0 // indirect 13 | golang.org/x/text v0.19.0 // indirect 14 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /endpoint/grpc_stream/testdata/main.go: -------------------------------------------------------------------------------- 1 | // 服务端实现 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | pb "main/api/ble/v1" 7 | "net" 8 | "time" 9 | 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/reflection" 12 | ) 13 | 14 | type dataServer struct { 15 | pb.UnimplementedDataServiceServer 16 | } 17 | 18 | func (s *dataServer) StreamData(req *pb.StreamRequest, stream pb.DataService_StreamDataServer) error { 19 | // 模拟发送不同类型的数据 20 | for { 21 | // 发送温度数据 22 | tempData := &pb.DataResponse{ 23 | Type: "temperature", 24 | Payload: []byte(`{"value": 25.5, "unit": "C"}`), 25 | Timestamp: time.Now().Unix(), 26 | } 27 | if err := stream.Send(tempData); err != nil { 28 | return err 29 | } 30 | 31 | time.Sleep(time.Second) 32 | 33 | // 发送湿度数据 34 | humidityData := &pb.DataResponse{ 35 | Type: "humidity", 36 | Payload: []byte(`{"value": 60, "unit": "%"}`), 37 | Timestamp: time.Now().Unix(), 38 | } 39 | if err := stream.Send(humidityData); err != nil { 40 | return err 41 | } 42 | 43 | time.Sleep(time.Second) 44 | } 45 | } 46 | 47 | func main() { 48 | lis, err := net.Listen("tcp", ":9000") 49 | if err != nil { 50 | panic(err) 51 | } 52 | 53 | grpcServer := grpc.NewServer() 54 | pb.RegisterDataServiceServer(grpcServer, &dataServer{}) 55 | 56 | // 注册反射服务 - 添加这行 57 | reflection.Register(grpcServer) 58 | 59 | fmt.Println("Server starting at :9000") 60 | if err := grpcServer.Serve(lis); err != nil { 61 | panic(err) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /endpoint/kafka/kafka_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package kafka 18 | 19 | import ( 20 | "github.com/IBM/sarama" 21 | "github.com/rulego/rulego" 22 | "github.com/rulego/rulego/api/types" 23 | endpointApi "github.com/rulego/rulego/api/types/endpoint" 24 | "github.com/rulego/rulego/endpoint" 25 | "github.com/rulego/rulego/test/assert" 26 | "os" 27 | "sync" 28 | "testing" 29 | "time" 30 | ) 31 | 32 | var testdataFolder = "../../testdata" 33 | 34 | func TestKafkaEndpointInit(t *testing.T) { 35 | config := rulego.NewConfig(types.WithDefaultPool()) 36 | _, err := endpoint.Registry.New(Type, config, Config{ 37 | Server: "", 38 | GroupId: "test01", 39 | }) 40 | assert.Equal(t, "brokers is empty", err.Error()) 41 | 42 | ep, err := endpoint.Registry.New(Type, config, Config{ 43 | Server: "localhost:9092", 44 | }) 45 | assert.Equal(t, "rulego", ep.(*Kafka).Config.GroupId) 46 | 47 | ep, err = endpoint.Registry.New(Type, config, Config{ 48 | Server: "localhost:9092,localhost:9093", 49 | }) 50 | assert.Equal(t, "localhost:9092", ep.(*Kafka).brokers[0]) 51 | assert.Equal(t, "localhost:9093", ep.(*Kafka).brokers[1]) 52 | 53 | ep, err = endpoint.Registry.New(Type, config, types.Configuration{ 54 | "brokers": []string{"localhost:9092", "localhost:9093"}, 55 | }) 56 | assert.Equal(t, "localhost:9092", ep.(*Kafka).brokers[0]) 57 | assert.Equal(t, "localhost:9093", ep.(*Kafka).brokers[1]) 58 | } 59 | 60 | func TestKafkaEndpoint(t *testing.T) { 61 | 62 | buf, err := os.ReadFile(testdataFolder + "/chain_msg_type_switch.json") 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | config := rulego.NewConfig(types.WithDefaultPool()) 67 | //注册规则链 68 | _, _ = rulego.New("default", buf, rulego.WithConfig(config)) 69 | 70 | //启动kafka接收服务 71 | kafkaEndpoint, err := endpoint.Registry.New(Type, config, Config{ 72 | Server: "localhost:9092", 73 | GroupId: "test01", 74 | }) 75 | //路由1 76 | router1 := endpoint.NewRouter().From("device.msg.request").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 77 | //fmt.Println("接收到数据:device.msg.request", exchange.In.GetMsg()) 78 | assert.Equal(t, "test message", exchange.In.GetMsg().Data) 79 | return true 80 | }).To("chain:default").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 81 | //往指定主题发送数据,用于响应 82 | exchange.Out.Headers().Add(KeyResponseTopic, "device.msg.response") 83 | exchange.Out.SetBody([]byte("this is response")) 84 | return true 85 | }).End() 86 | 87 | //模拟获取响应 88 | router2 := endpoint.NewRouter().From("device.msg.response").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 89 | //fmt.Println("接收到数据:device.msg.response", exchange.In.GetMsg()) 90 | assert.Equal(t, "this is response", exchange.In.GetMsg().Data) 91 | return true 92 | }).End() 93 | 94 | router3 := endpoint.NewRouter().From("device.msg.response").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 95 | //fmt.Println("接收到数据:device.msg.response", exchange.In.GetMsg()) 96 | assert.Equal(t, "this is response", exchange.In.GetMsg().Data) 97 | return true 98 | }).End() 99 | 100 | //注册路由 101 | _, err = kafkaEndpoint.AddRouter(router1) 102 | _, err = kafkaEndpoint.AddRouter(router2) 103 | if err != nil { 104 | panic(err) 105 | } 106 | _, err = kafkaEndpoint.AddRouter(router3) 107 | assert.NotNil(t, err) 108 | //并启动服务 109 | err = kafkaEndpoint.Start() 110 | if err != nil { 111 | panic(err) 112 | } 113 | 114 | // 测试发布和订阅 115 | producer, err := sarama.NewSyncProducer([]string{"localhost:9092"}, nil) 116 | if err != nil { 117 | t.Fatal("Failed to start Sarama producer:", err) 118 | } 119 | defer producer.Close() 120 | 121 | consumer, err := sarama.NewConsumer([]string{"localhost:9092"}, nil) 122 | if err != nil { 123 | t.Fatal("Failed to start Sarama consumer:", err) 124 | } 125 | defer consumer.Close() 126 | var wg sync.WaitGroup 127 | wg.Add(1) 128 | 129 | go func(g *sync.WaitGroup) { 130 | // 创建消费者来读取 device.msg.response 131 | partitionConsumer, err := consumer.ConsumePartition("device.msg.response", 0, sarama.OffsetNewest) 132 | if err != nil { 133 | t.Fatal("Failed to start consumer for response topic:", err) 134 | } 135 | defer partitionConsumer.Close() 136 | 137 | // 等待并验证响应 138 | select { 139 | case msg := <-partitionConsumer.Messages(): 140 | assert.Equal(t, "this is response", string(msg.Value)) 141 | g.Done() 142 | case <-time.After(5 * time.Second): 143 | g.Done() 144 | t.Fatal("Failed to receive message within the timeout period") 145 | } 146 | }(&wg) 147 | 148 | time.Sleep(time.Second) 149 | // 发布消息到 device.msg.request 150 | _, _, err = producer.SendMessage(&sarama.ProducerMessage{ 151 | Topic: "device.msg.request", 152 | Value: sarama.StringEncoder("test message"), 153 | }) 154 | if err != nil { 155 | t.Fatal("Failed to send message:", err) 156 | } 157 | wg.Wait() 158 | kafkaEndpoint.Destroy() 159 | } 160 | -------------------------------------------------------------------------------- /endpoint/nats/nats.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package nats 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | "github.com/nats-io/nats.go" 24 | "github.com/rulego/rulego/api/types" 25 | endpointApi "github.com/rulego/rulego/api/types/endpoint" 26 | "github.com/rulego/rulego/components/base" 27 | "github.com/rulego/rulego/endpoint" 28 | "github.com/rulego/rulego/endpoint/impl" 29 | "github.com/rulego/rulego/utils/maps" 30 | "github.com/rulego/rulego/utils/runtime" 31 | "net/textproto" 32 | ) 33 | 34 | // Type 组件类型 35 | const Type = types.EndpointTypePrefix + "nats" 36 | 37 | // KeyResponseTopic 响应主题metadataKey 38 | const KeyResponseTopic = "responseTopic" 39 | 40 | // Endpoint 别名 41 | type Endpoint = Nats 42 | 43 | var _ endpointApi.Endpoint = (*Endpoint)(nil) 44 | 45 | // 注册组件 46 | func init() { 47 | _ = endpoint.Registry.Register(&Endpoint{}) 48 | } 49 | 50 | // RequestMessage 请求消息 51 | type RequestMessage struct { 52 | request *nats.Msg 53 | msg *types.RuleMsg 54 | err error 55 | } 56 | 57 | func (r *RequestMessage) Body() []byte { 58 | return r.request.Data 59 | } 60 | 61 | func (r *RequestMessage) Headers() textproto.MIMEHeader { 62 | header := make(textproto.MIMEHeader) 63 | header.Set("topic", r.request.Subject) 64 | return header 65 | } 66 | 67 | func (r *RequestMessage) From() string { 68 | return r.request.Subject 69 | } 70 | 71 | func (r *RequestMessage) GetParam(key string) string { 72 | return "" 73 | } 74 | 75 | func (r *RequestMessage) SetMsg(msg *types.RuleMsg) { 76 | r.msg = msg 77 | } 78 | 79 | func (r *RequestMessage) GetMsg() *types.RuleMsg { 80 | if r.msg == nil { 81 | // 默认指定是JSON格式,如果不是该类型,请在process函数中修改 82 | ruleMsg := types.NewMsg(0, r.From(), types.JSON, types.NewMetadata(), string(r.Body())) 83 | ruleMsg.Metadata.PutValue("topic", r.From()) 84 | r.msg = &ruleMsg 85 | } 86 | return r.msg 87 | } 88 | 89 | func (r *RequestMessage) SetStatusCode(statusCode int) { 90 | } 91 | 92 | func (r *RequestMessage) SetBody(body []byte) { 93 | } 94 | 95 | func (r *RequestMessage) SetError(err error) { 96 | r.err = err 97 | } 98 | 99 | func (r *RequestMessage) GetError() error { 100 | return r.err 101 | } 102 | 103 | // ResponseMessage 响应消息 104 | type ResponseMessage struct { 105 | request *nats.Msg 106 | response *nats.Conn 107 | body []byte 108 | msg *types.RuleMsg 109 | headers textproto.MIMEHeader 110 | err error 111 | log func(format string, v ...interface{}) 112 | } 113 | 114 | func (r *ResponseMessage) Body() []byte { 115 | return r.body 116 | } 117 | 118 | func (r *ResponseMessage) Headers() textproto.MIMEHeader { 119 | if r.headers == nil { 120 | r.headers = make(map[string][]string) 121 | } 122 | return r.headers 123 | } 124 | 125 | func (r *ResponseMessage) From() string { 126 | return r.request.Subject 127 | } 128 | 129 | func (r *ResponseMessage) GetParam(key string) string { 130 | return "" 131 | } 132 | 133 | func (r *ResponseMessage) SetMsg(msg *types.RuleMsg) { 134 | r.msg = msg 135 | } 136 | 137 | func (r *ResponseMessage) GetMsg() *types.RuleMsg { 138 | return r.msg 139 | } 140 | 141 | func (r *ResponseMessage) SetStatusCode(statusCode int) { 142 | } 143 | 144 | // 从msg.Metadata或者响应头获取 145 | func (r *ResponseMessage) getMetadataValue(metadataName, headerName string) string { 146 | var v string 147 | if r.GetMsg() != nil { 148 | metadata := r.GetMsg().Metadata 149 | v = metadata.GetValue(metadataName) 150 | } 151 | if v == "" { 152 | return r.Headers().Get(headerName) 153 | } else { 154 | return v 155 | } 156 | } 157 | 158 | func (r *ResponseMessage) SetBody(body []byte) { 159 | r.body = body 160 | topic := r.getMetadataValue(KeyResponseTopic, KeyResponseTopic) 161 | if topic != "" { 162 | err := r.response.Publish(topic, r.body) 163 | if err != nil { 164 | r.SetError(err) 165 | } 166 | } 167 | } 168 | 169 | func (r *ResponseMessage) SetError(err error) { 170 | r.err = err 171 | } 172 | 173 | func (r *ResponseMessage) GetError() error { 174 | return r.err 175 | } 176 | 177 | type Config struct { 178 | // NATS服务器地址 179 | Server string 180 | // NATS用户名 181 | Username string 182 | // NATS密码 183 | Password string 184 | } 185 | 186 | // Nats NATS接收端端点 187 | type Nats struct { 188 | impl.BaseEndpoint 189 | base.SharedNode[*nats.Conn] 190 | RuleConfig types.Config 191 | //Config 配置 192 | Config Config 193 | // NATS连接 194 | conn *nats.Conn 195 | // 订阅映射关系,用于取消订阅 196 | subscriptions map[string]*nats.Subscription 197 | } 198 | 199 | // Type 组件类型 200 | func (x *Nats) Type() string { 201 | return Type 202 | } 203 | 204 | func (x *Nats) Id() string { 205 | return x.Config.Server 206 | } 207 | 208 | func (x *Nats) New() types.Node { 209 | return &Nats{ 210 | Config: Config{ 211 | Server: "nats://127.0.0.1:4222", 212 | }, 213 | } 214 | } 215 | 216 | // Init 初始化 217 | func (x *Nats) Init(ruleConfig types.Config, configuration types.Configuration) error { 218 | err := maps.Map2Struct(configuration, &x.Config) 219 | x.RuleConfig = ruleConfig 220 | _ = x.SharedNode.Init(x.RuleConfig, x.Type(), x.Config.Server, true, func() (*nats.Conn, error) { 221 | return x.initClient() 222 | }) 223 | return err 224 | } 225 | 226 | // Destroy 销毁 227 | func (x *Nats) Destroy() { 228 | _ = x.Close() 229 | } 230 | 231 | func (x *Nats) Close() error { 232 | if nil != x.conn { 233 | x.conn.Close() 234 | } 235 | x.BaseEndpoint.Destroy() 236 | return nil 237 | } 238 | 239 | func (x *Nats) AddRouter(router endpointApi.Router, params ...interface{}) (string, error) { 240 | if router == nil { 241 | return "", errors.New("router cannot be nil") 242 | } 243 | client, err := x.SharedNode.Get() 244 | if err != nil { 245 | return "", err 246 | } 247 | routerId := router.GetId() 248 | if routerId == "" { 249 | routerId = router.GetFrom().ToString() 250 | router.SetId(routerId) 251 | } 252 | x.Lock() 253 | defer x.Unlock() 254 | if x.subscriptions == nil { 255 | x.subscriptions = make(map[string]*nats.Subscription) 256 | } 257 | if _, ok := x.subscriptions[routerId]; ok { 258 | return routerId, fmt.Errorf("routerId %s already exists", routerId) 259 | } 260 | subscription, err := client.Subscribe(router.FromToString(), func(msg *nats.Msg) { 261 | defer func() { 262 | if e := recover(); e != nil { 263 | x.Printf("nats endpoint handler err :\n%v", runtime.Stack()) 264 | } 265 | }() 266 | exchange := &endpointApi.Exchange{ 267 | In: &RequestMessage{ 268 | request: msg, 269 | }, 270 | Out: &ResponseMessage{ 271 | request: msg, 272 | response: client, 273 | log: func(format string, v ...interface{}) { 274 | x.Printf(format, v...) 275 | }, 276 | }, 277 | } 278 | x.DoProcess(context.Background(), router, exchange) 279 | }) 280 | if err != nil { 281 | return "", err 282 | } 283 | x.subscriptions[routerId] = subscription 284 | return routerId, nil 285 | } 286 | 287 | func (x *Nats) RemoveRouter(routerId string, params ...interface{}) error { 288 | x.Lock() 289 | defer x.Unlock() 290 | if subscription, ok := x.subscriptions[routerId]; ok { 291 | delete(x.subscriptions, routerId) 292 | return subscription.Unsubscribe() 293 | } 294 | return errors.New("router not found") 295 | } 296 | 297 | func (x *Nats) Start() error { 298 | if !x.SharedNode.IsInit() { 299 | return x.SharedNode.Init(x.RuleConfig, x.Type(), x.Config.Server, true, func() (*nats.Conn, error) { 300 | return x.initClient() 301 | }) 302 | } 303 | return nil 304 | } 305 | 306 | func (x *Nats) Printf(format string, v ...interface{}) { 307 | if x.RuleConfig.Logger != nil { 308 | x.RuleConfig.Logger.Printf(format, v...) 309 | } 310 | } 311 | 312 | func (x *Nats) initClient() (*nats.Conn, error) { 313 | if x.conn != nil { 314 | return x.conn, nil 315 | } else { 316 | x.Locker.Lock() 317 | defer x.Locker.Unlock() 318 | if x.conn != nil { 319 | return x.conn, nil 320 | } 321 | var err error 322 | x.conn, err = nats.Connect(x.Config.Server, nats.UserInfo(x.Config.Username, x.Config.Password)) 323 | return x.conn, err 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /endpoint/nats/nats_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package nats 18 | 19 | import ( 20 | "github.com/rulego/rulego" 21 | "github.com/rulego/rulego/test/assert" 22 | "os" 23 | "sync/atomic" 24 | "testing" 25 | "time" 26 | 27 | "github.com/nats-io/nats.go" 28 | "github.com/rulego/rulego/api/types" 29 | endpointApi "github.com/rulego/rulego/api/types/endpoint" 30 | "github.com/rulego/rulego/endpoint" 31 | ) 32 | 33 | var testdataFolder = "../../testdata" 34 | 35 | func TestNatsEndpoint(t *testing.T) { 36 | buf, err := os.ReadFile(testdataFolder + "/chain_msg_type_switch.json") 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | config := rulego.NewConfig(types.WithDefaultPool()) 41 | // 注册规则链 42 | _, _ = rulego.New("default", buf, rulego.WithConfig(config)) 43 | 44 | // 启动NATS接收服务 45 | natsEndpoint, err := endpoint.Registry.New(Type, config, Config{ 46 | Server: "nats://localhost:4222", 47 | }) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | // 路由1 53 | router1 := endpoint.NewRouter().From("device.msg.request").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 54 | assert.Equal(t, "test message", exchange.In.GetMsg().Data) 55 | return true 56 | }).To("chain:default").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 57 | // 往指定主题发送数据,用于响应 58 | exchange.Out.Headers().Add(KeyResponseTopic, "device.msg.response") 59 | exchange.Out.SetBody([]byte("this is response")) 60 | return true 61 | }).End() 62 | 63 | count := int32(0) 64 | // 模拟获取响应 65 | router2 := endpoint.NewRouter().SetId("router3").From("device.msg.response").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 66 | //fmt.Println("接收到数据:device.msg.response", exchange.In.GetMsg()) 67 | assert.Equal(t, "this is response", exchange.In.GetMsg().Data) 68 | atomic.AddInt32(&count, 1) 69 | return true 70 | }).End() 71 | 72 | // 模拟获取响应,相同主题 73 | router3 := endpoint.NewRouter().SetId("router3").From("device.msg.response").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 74 | //fmt.Println("接收到数据:device.msg.response", exchange.In.GetMsg()) 75 | assert.Equal(t, "this is response", exchange.In.GetMsg().Data) 76 | atomic.AddInt32(&count, 1) 77 | return true 78 | }).End() 79 | 80 | // 注册路由 81 | _, err = natsEndpoint.AddRouter(router1) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | _, err = natsEndpoint.AddRouter(router2) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | router3Id, err := natsEndpoint.AddRouter(router3) 90 | assert.NotNil(t, err) 91 | // 启动服务 92 | err = natsEndpoint.Start() 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | // 测试发布和订阅 98 | conn, _ := nats.Connect(nats.DefaultURL) 99 | defer conn.Close() 100 | 101 | // 发布消息到device.msg.request 102 | err = conn.Publish("device.msg.request", []byte("test message")) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | // 等待消息处理 107 | time.Sleep(time.Second * 1) 108 | 109 | assert.Equal(t, int32(1), count) 110 | 111 | count = 0 112 | //删除一个相同的主题 113 | _ = natsEndpoint.RemoveRouter(router3Id) 114 | // 发布消息到device.msg.request 115 | err = conn.Publish("device.msg.request", []byte("test message")) 116 | if err != nil { 117 | t.Fatal(err) 118 | } 119 | // 等待消息处理 120 | time.Sleep(time.Second * 1) 121 | 122 | assert.Equal(t, int32(0), count) 123 | 124 | } 125 | -------------------------------------------------------------------------------- /endpoint/rabbitmq/rabbitmq_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package rabbitmq 18 | 19 | import ( 20 | amqp "github.com/rabbitmq/amqp091-go" 21 | "github.com/rulego/rulego" 22 | "github.com/rulego/rulego/test/assert" 23 | "os" 24 | "sync/atomic" 25 | "testing" 26 | "time" 27 | 28 | "github.com/rulego/rulego/api/types" 29 | endpointApi "github.com/rulego/rulego/api/types/endpoint" 30 | "github.com/rulego/rulego/endpoint" 31 | ) 32 | 33 | var testdataFolder = "../../testdata" 34 | 35 | const ( 36 | server = "amqp://guest:guest@8.134.32.225:5672/" 37 | exchange = "rulego.topic.test" 38 | topicRequest = "device.msg.request" 39 | topicResponse = "device.msg.response" 40 | ) 41 | 42 | func TestEndpoint(t *testing.T) { 43 | buf, err := os.ReadFile(testdataFolder + "/chain_msg_type_switch.json") 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | config := rulego.NewConfig(types.WithDefaultPool()) 48 | // 注册规则链 49 | _, _ = rulego.New("default", buf, rulego.WithConfig(config)) 50 | 51 | // 启动enpoint接收服务 52 | ep, err := endpoint.Registry.New(Type, config, Config{ 53 | Server: server, 54 | Exchange: exchange, 55 | }) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | // 路由1 61 | router1 := endpoint.NewRouter().From(topicRequest).Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 62 | assert.Equal(t, "test message", exchange.In.GetMsg().Data) 63 | return true 64 | }).To("chain:default").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 65 | // 往指定主题发送数据,用于响应 66 | exchange.Out.Headers().Add(KeyResponseTopic, topicResponse) 67 | exchange.Out.SetBody([]byte("this is response")) 68 | return true 69 | }).End() 70 | 71 | count := int32(0) 72 | // 模拟获取响应 73 | router2 := endpoint.NewRouter().SetId("router3").From(topicResponse).Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 74 | //fmt.Println("接收到数据:device.msg.response", exchange.In.GetMsg()) 75 | assert.Equal(t, "this is response", exchange.In.GetMsg().Data) 76 | atomic.AddInt32(&count, 1) 77 | return true 78 | }).End() 79 | 80 | // 模拟获取响应,相同主题 81 | router3 := endpoint.NewRouter().SetId("router3").From(topicResponse).Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 82 | assert.Equal(t, "this is response", exchange.In.GetMsg().Data) 83 | atomic.AddInt32(&count, 1) 84 | return true 85 | }).End() 86 | 87 | // 注册路由 88 | _, err = ep.AddRouter(router1) 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | _, err = ep.AddRouter(router2) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | router3Id, err := ep.AddRouter(router3) 97 | assert.NotNil(t, err) 98 | // 启动服务 99 | err = ep.Start() 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | // 测试发布和订阅 105 | conn, err := amqp.Dial(server) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | defer conn.Close() 110 | channel, err := conn.Channel() 111 | if err != nil { 112 | t.Fatal(err) 113 | } 114 | defer channel.Close() 115 | 116 | // 发布消息到device.msg.request 117 | err = channel.Publish( 118 | exchange, // 发布到的交换机 119 | topicRequest, // 路由键 120 | false, // 表示是否要求消息必须被路由到至少一个队列 121 | false, // 是否要求消息立即被消费者接收 122 | amqp.Publishing{ 123 | ContentType: ContentTypeJson, 124 | ContentEncoding: KeyUTF8, 125 | Body: []byte("test message"), 126 | }) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | // 等待消息处理 131 | time.Sleep(time.Second * 1) 132 | 133 | assert.Equal(t, int32(1), count) 134 | 135 | count = 0 136 | //删除一个相同的主题 137 | _ = ep.RemoveRouter(router3Id) 138 | // 发布消息到device.msg.request 139 | err = channel.Publish( 140 | exchange, // 发布到的交换机 141 | topicRequest, // 路由键 142 | false, // 表示是否要求消息必须被路由到至少一个队列 143 | false, // 是否要求消息立即被消费者接收 144 | amqp.Publishing{ 145 | ContentType: ContentTypeJson, 146 | ContentEncoding: KeyUTF8, 147 | Body: []byte("test message"), 148 | }) 149 | if err != nil { 150 | t.Fatal(err) 151 | } 152 | // 等待消息处理 153 | time.Sleep(time.Second * 1) 154 | 155 | assert.Equal(t, int32(0), count) 156 | 157 | } 158 | -------------------------------------------------------------------------------- /endpoint/redis/redis_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package redis 18 | 19 | import ( 20 | "context" 21 | "github.com/redis/go-redis/v9" 22 | "github.com/rulego/rulego" 23 | "github.com/rulego/rulego/test/assert" 24 | "os" 25 | "sync/atomic" 26 | "testing" 27 | "time" 28 | 29 | "github.com/rulego/rulego/api/types" 30 | endpointApi "github.com/rulego/rulego/api/types/endpoint" 31 | "github.com/rulego/rulego/endpoint" 32 | ) 33 | 34 | var testdataFolder = "../../testdata" 35 | 36 | func TestRedisEndpoint(t *testing.T) { 37 | buf, err := os.ReadFile(testdataFolder + "/chain_msg_type_switch.json") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | config := rulego.NewConfig(types.WithDefaultPool()) 42 | // 注册规则链 43 | _, _ = rulego.New("default", buf, rulego.WithConfig(config)) 44 | 45 | // 启动redis接收服务 46 | ep, err := endpoint.Registry.New(Type, config, Config{ 47 | Server: "127.0.0.1:6379", 48 | }) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | count := int32(0) 53 | // 路由1 54 | router1 := endpoint.NewRouter().SetId("router1").From("device.msg.request,device.msg.response").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 55 | atomic.AddInt32(&count, 1) 56 | if exchange.In.Headers().Get("topic") == "device.msg.response" { 57 | assert.Equal(t, "this is response", exchange.In.GetMsg().Data) 58 | return false 59 | } 60 | assert.Equal(t, "test message", exchange.In.GetMsg().Data) 61 | return true 62 | }).To("chain:default").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 63 | // 往指定主题发送数据,用于响应 64 | exchange.Out.Headers().Add(KeyResponseTopic, "device.msg.response") 65 | exchange.Out.SetBody([]byte("this is response")) 66 | return true 67 | }).End() 68 | //重复路由,无法注册 69 | router2 := endpoint.NewRouter().SetId("router1").From("device.msg.request,device.msg.response").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 70 | atomic.AddInt32(&count, 1) 71 | if exchange.In.Headers().Get("topic") == "device.msg.response" { 72 | assert.Equal(t, "this is response", exchange.In.GetMsg().Data) 73 | return false 74 | } 75 | assert.Equal(t, "test message", exchange.In.GetMsg().Data) 76 | return true 77 | }).To("chain:default").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 78 | // 往指定主题发送数据,用于响应 79 | exchange.Out.Headers().Add(KeyResponseTopic, "device.msg.response") 80 | exchange.Out.SetBody([]byte("this is response")) 81 | return true 82 | }).End() 83 | 84 | // 注册路由 85 | _, err = ep.AddRouter(router1) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | _, err = ep.AddRouter(router2) 90 | assert.NotNil(t, err) 91 | // 启动服务 92 | err = ep.Start() 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | // 测试发布和订阅 98 | redisClient := redis.NewClient(&redis.Options{ 99 | Addr: "127.0.0.1:6379", 100 | }) 101 | err = redisClient.Ping(context.Background()).Err() 102 | assert.Nil(t, err) 103 | // 发布消息到device.msg.request 104 | redisClient.Publish(context.TODO(), "device.msg.request", "test message") 105 | // 等待消息处理 106 | time.Sleep(time.Millisecond * 200) 107 | assert.Equal(t, int32(2), count) 108 | atomic.StoreInt32(&count, 0) 109 | 110 | router3 := endpoint.NewRouter().SetId("router3").From("device.msg.request").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 111 | atomic.AddInt32(&count, 1) 112 | if exchange.In.Headers().Get("topic") == "device.msg.response" { 113 | assert.Equal(t, "this is response", exchange.In.GetMsg().Data) 114 | return false 115 | } 116 | assert.Equal(t, "test message", exchange.In.GetMsg().Data) 117 | return true 118 | }).To("chain:default").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 119 | // 往指定主题发送数据,用于响应 120 | exchange.Out.Headers().Add(KeyResponseTopic, "device.msg.response") 121 | exchange.Out.SetBody([]byte("this is response")) 122 | return true 123 | }).End() 124 | 125 | _, err = ep.AddRouter(router3) 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | // 发布消息到device.msg.request 130 | redisClient.Publish(context.TODO(), "device.msg.request", "test message") 131 | // 等待消息处理 132 | time.Sleep(time.Millisecond * 200) 133 | assert.Equal(t, int32(4), count) 134 | atomic.StoreInt32(&count, 0) 135 | 136 | _ = ep.RemoveRouter("router3") 137 | 138 | redisClient.Publish(context.TODO(), "device.msg.request", "test message") 139 | // 等待消息处理 140 | time.Sleep(time.Millisecond * 200) 141 | assert.Equal(t, int32(2), count) 142 | atomic.StoreInt32(&count, 0) 143 | 144 | _ = ep.RemoveRouter("router1") 145 | 146 | redisClient.Publish(context.TODO(), "device.msg.request", "test message") 147 | // 等待消息处理 148 | time.Sleep(time.Millisecond * 200) 149 | assert.Equal(t, int32(0), count) 150 | atomic.StoreInt32(&count, 0) 151 | 152 | _, _ = ep.AddRouter(router1) 153 | 154 | redisClient.Publish(context.TODO(), "device.msg.request", "test message") 155 | // 等待消息处理 156 | time.Sleep(time.Millisecond * 200) 157 | assert.Equal(t, int32(2), count) 158 | atomic.StoreInt32(&count, 0) 159 | } 160 | -------------------------------------------------------------------------------- /endpoint/redis_stream/redis_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package redis 18 | 19 | import ( 20 | "context" 21 | "github.com/redis/go-redis/v9" 22 | "github.com/rulego/rulego" 23 | "github.com/rulego/rulego/test/assert" 24 | "os" 25 | "sync/atomic" 26 | "testing" 27 | "time" 28 | 29 | "github.com/rulego/rulego/api/types" 30 | endpointApi "github.com/rulego/rulego/api/types/endpoint" 31 | "github.com/rulego/rulego/endpoint" 32 | ) 33 | 34 | var testdataFolder = "../../testdata" 35 | 36 | func Test2t(*testing.T) { 37 | // 测试发布和订阅 38 | redisClient := redis.NewClient(&redis.Options{ 39 | Addr: "127.0.0.1:6379", 40 | }) 41 | addData(redisClient, "device.msg.request") 42 | } 43 | func TestRedisEndpoint(t *testing.T) { 44 | buf, err := os.ReadFile(testdataFolder + "/chain_msg_type_switch.json") 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | config := rulego.NewConfig(types.WithDefaultPool()) 49 | // 注册规则链 50 | _, _ = rulego.New("default", buf, rulego.WithConfig(config)) 51 | 52 | // 启动redis接收服务 53 | ep, err := endpoint.Registry.New(Type, config, Config{ 54 | Server: "127.0.0.1:6379", 55 | }) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | count := int32(0) 60 | // 路由1 61 | router1 := endpoint.NewRouter().SetId("router1").From("device.msg.request,device.msg.response").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 62 | atomic.AddInt32(&count, 1) 63 | if exchange.In.Headers().Get("topic") == "device.msg.response" { 64 | assert.Equal(t, "{\"value\":\"this is response\"}", exchange.In.GetMsg().Data) 65 | return false 66 | } 67 | assert.Equal(t, "{\"field1\":\"value1\",\"field2\":\"42\"}", exchange.In.GetMsg().Data) 68 | return true 69 | }).To("chain:default").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 70 | // 往指定主题发送数据,用于响应 71 | exchange.Out.Headers().Add(KeyResponseTopic, "device.msg.response") 72 | exchange.Out.SetBody([]byte("this is response")) 73 | return true 74 | }).End() 75 | //重复路由,无法注册 76 | router2 := endpoint.NewRouter().SetId("router1").From("device.msg.request,device.msg.response").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 77 | atomic.AddInt32(&count, 1) 78 | if exchange.In.Headers().Get("topic") == "device.msg.response" { 79 | assert.Equal(t, "{\"value\":\"this is response\"}", exchange.In.GetMsg().Data) 80 | return false 81 | } 82 | assert.Equal(t, "{\"field1\":\"value1\",\"field2\":\"42\"}", exchange.In.GetMsg().Data) 83 | return true 84 | }).To("chain:default").Process(func(router endpointApi.Router, exchange *endpointApi.Exchange) bool { 85 | // 往指定主题发送数据,用于响应 86 | exchange.Out.Headers().Add(KeyResponseTopic, "device.msg.response") 87 | exchange.Out.SetBody([]byte("this is response")) 88 | return true 89 | }).End() 90 | 91 | // 注册路由 92 | _, err = ep.AddRouter(router1) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | _, err = ep.AddRouter(router2) 97 | assert.NotNil(t, err) 98 | // 启动服务 99 | err = ep.Start() 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | // 测试发布和订阅 105 | redisClient := redis.NewClient(&redis.Options{ 106 | Addr: "127.0.0.1:6379", 107 | }) 108 | err = redisClient.Ping(context.Background()).Err() 109 | assert.Nil(t, err) 110 | // 发布数据 111 | addData(redisClient, "device.msg.request") 112 | // 等待消息处理 113 | time.Sleep(time.Millisecond * 500) 114 | 115 | assert.Equal(t, int32(2), count) 116 | atomic.StoreInt32(&count, 0) 117 | 118 | ep.RemoveRouter("router1") 119 | // 发布数据 120 | addData(redisClient, "device.msg.request") 121 | // 等待消息处理 122 | time.Sleep(time.Millisecond * 500) 123 | assert.Equal(t, int32(0), count) 124 | atomic.StoreInt32(&count, 0) 125 | } 126 | 127 | func addData(redisClient *redis.Client, streamName string) { 128 | // 定义要写入的消息,这里是一个简单的 key-value 形式 129 | // Redis 会自动为每个消息生成一个唯一的 ID 130 | message := map[string]interface{}{ 131 | "field1": "value1", 132 | "field2": 42, 133 | } 134 | 135 | // 向 Stream 写入数据 136 | redisClient.XAdd(context.Background(), &redis.XAddArgs{ 137 | Stream: streamName, 138 | ID: "*", // 使用 "*" 表示使用 Redis 自动生成的 ID 139 | Values: message, 140 | }).Result() 141 | } 142 | -------------------------------------------------------------------------------- /endpoint/wukongim/wukongim.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package wukongim 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "net/textproto" 23 | "time" 24 | 25 | wkproto "github.com/WuKongIM/WuKongIMGoProto" 26 | "github.com/WuKongIM/WuKongIMGoSDK/pkg/wksdk" 27 | "github.com/rulego/rulego/api/types" 28 | endpointApi "github.com/rulego/rulego/api/types/endpoint" 29 | "github.com/rulego/rulego/components/base" 30 | "github.com/rulego/rulego/endpoint" 31 | "github.com/rulego/rulego/endpoint/impl" 32 | "github.com/rulego/rulego/utils/maps" 33 | ) 34 | 35 | // Type 组件类型 36 | const Type = types.EndpointTypePrefix + "wukongim" 37 | 38 | // Endpoint 别名 39 | type Endpoint = Wukongim 40 | 41 | var _ endpointApi.Endpoint = (*Endpoint)(nil) 42 | 43 | // 注册组件 44 | func init() { 45 | _ = endpoint.Registry.Register(&Endpoint{}) 46 | } 47 | 48 | // RequestMessage http请求消息 49 | type RequestMessage struct { 50 | headers textproto.MIMEHeader 51 | body []byte 52 | msg *types.RuleMsg 53 | err error 54 | } 55 | 56 | func (r *RequestMessage) Body() []byte { 57 | return r.body 58 | } 59 | 60 | func (r *RequestMessage) Headers() textproto.MIMEHeader { 61 | header := make(textproto.MIMEHeader) 62 | return header 63 | } 64 | 65 | func (r *RequestMessage) From() string { 66 | return "" 67 | } 68 | 69 | func (r *RequestMessage) GetParam(key string) string { 70 | return "" 71 | } 72 | 73 | func (r *RequestMessage) SetMsg(msg *types.RuleMsg) { 74 | r.msg = msg 75 | } 76 | 77 | func (r *RequestMessage) GetMsg() *types.RuleMsg { 78 | if r.msg == nil { 79 | //默认指定是JSON格式,如果不是该类型,请在process函数中修改 80 | ruleMsg := types.NewMsg(0, r.From(), types.JSON, types.NewMetadata(), string(r.Body())) 81 | r.msg = &ruleMsg 82 | } 83 | return r.msg 84 | } 85 | 86 | func (r *RequestMessage) SetStatusCode(statusCode int) { 87 | } 88 | 89 | func (r *RequestMessage) SetBody(body []byte) { 90 | } 91 | 92 | func (r *RequestMessage) SetError(err error) { 93 | r.err = err 94 | } 95 | 96 | func (r *RequestMessage) GetError() error { 97 | return r.err 98 | } 99 | 100 | // ResponseMessage http响应消息 101 | type ResponseMessage struct { 102 | headers textproto.MIMEHeader 103 | body []byte 104 | msg *types.RuleMsg 105 | statusCode int 106 | err error 107 | } 108 | 109 | func (r *ResponseMessage) Body() []byte { 110 | return r.body 111 | } 112 | 113 | func (r *ResponseMessage) Headers() textproto.MIMEHeader { 114 | if r.headers == nil { 115 | r.headers = make(map[string][]string) 116 | } 117 | return r.headers 118 | } 119 | 120 | func (r *ResponseMessage) From() string { 121 | return "" 122 | } 123 | 124 | func (r *ResponseMessage) GetParam(key string) string { 125 | return "" 126 | } 127 | 128 | func (r *ResponseMessage) SetMsg(msg *types.RuleMsg) { 129 | r.msg = msg 130 | } 131 | func (r *ResponseMessage) GetMsg() *types.RuleMsg { 132 | return r.msg 133 | } 134 | 135 | // 从msg.Metadata或者响应头获取 136 | func (r *ResponseMessage) getMetadataValue(metadataName, headerName string) string { 137 | var v string 138 | if r.GetMsg() != nil { 139 | metadata := r.GetMsg().Metadata 140 | v = metadata.GetValue(metadataName) 141 | } 142 | if v == "" { 143 | return r.Headers().Get(headerName) 144 | } else { 145 | return v 146 | } 147 | } 148 | func (r *ResponseMessage) SetBody(body []byte) { 149 | r.body = body 150 | } 151 | 152 | func (r *ResponseMessage) SetError(err error) { 153 | r.err = err 154 | } 155 | 156 | func (r *ResponseMessage) GetError() error { 157 | return r.err 158 | } 159 | func (r *ResponseMessage) SetStatusCode(statusCode int) { 160 | r.statusCode = statusCode 161 | } 162 | 163 | type Config struct { 164 | // Wukongim服务器地址 165 | Server string 166 | // 用户UID 167 | UID string 168 | // 登录密码 169 | Token string 170 | // 连接超时,单位秒 171 | ConnectTimeout int64 172 | // Proto版本 173 | ProtoVersion int 174 | // 心跳间隔,单位秒 175 | PingInterval int64 176 | // 是否自动重连 177 | Reconnect bool 178 | // 是否自动确认 179 | AutoAck bool 180 | } 181 | 182 | // Wukongim 接收端端点 183 | type Wukongim struct { 184 | impl.BaseEndpoint 185 | base.SharedNode[*wksdk.Client] 186 | RuleConfig types.Config 187 | //Config 配置 188 | Config Config 189 | client *wksdk.Client 190 | Router endpointApi.Router 191 | } 192 | 193 | // Type 组件类型 194 | func (x *Wukongim) Type() string { 195 | return Type 196 | } 197 | 198 | func (x *Wukongim) New() types.Node { 199 | return &Wukongim{ 200 | Config: Config{ 201 | Server: "tcp://175.27.245.108:15100", 202 | UID: "test1", 203 | Token: "test1", 204 | ConnectTimeout: 5, 205 | ProtoVersion: wkproto.LatestVersion, 206 | PingInterval: 30, 207 | Reconnect: true, 208 | AutoAck: true, 209 | }, 210 | } 211 | } 212 | 213 | // Init 初始化 214 | func (x *Wukongim) Init(ruleConfig types.Config, configuration types.Configuration) error { 215 | err := maps.Map2Struct(configuration, &x.Config) 216 | x.RuleConfig = ruleConfig 217 | _ = x.SharedNode.Init(x.RuleConfig, x.Type(), x.Config.Server, true, func() (*wksdk.Client, error) { 218 | return x.initClient() 219 | }) 220 | return err 221 | } 222 | 223 | // Destroy 销毁组件 224 | func (x *Wukongim) Destroy() { 225 | if x.client != nil { 226 | _ = x.client.Disconnect() 227 | } 228 | } 229 | 230 | func (x *Wukongim) Close() error { 231 | x.BaseEndpoint.Destroy() 232 | return nil 233 | } 234 | 235 | func (x *Wukongim) Id() string { 236 | return x.Config.Server 237 | } 238 | 239 | func (x *Wukongim) AddRouter(router endpointApi.Router, params ...interface{}) (string, error) { 240 | if router == nil { 241 | return "", errors.New("router cannot be nil") 242 | } 243 | if x.Router != nil { 244 | return "", errors.New("duplicate router") 245 | } 246 | x.Router = router 247 | return router.GetId(), nil 248 | } 249 | 250 | func (x *Wukongim) RemoveRouter(routerId string, params ...interface{}) error { 251 | x.Lock() 252 | defer x.Unlock() 253 | x.Router = nil 254 | return nil 255 | } 256 | 257 | func (x *Wukongim) Start() error { 258 | var err error 259 | if !x.SharedNode.IsInit() { 260 | err = x.SharedNode.Init(x.RuleConfig, x.Type(), x.Config.Server, true, func() (*wksdk.Client, error) { 261 | return x.initClient() 262 | }) 263 | } 264 | x.client.OnMessage(func(msg *wksdk.Message) { 265 | if !x.Config.AutoAck { 266 | err = msg.Ack() 267 | if err != nil { 268 | x.Printf("msg ack failed,msg: %v, err: %s", msg, err) 269 | } 270 | } 271 | exchange := &endpoint.Exchange{ 272 | In: &RequestMessage{body: []byte(string(msg.Payload))}, 273 | Out: &ResponseMessage{ 274 | body: []byte(string(msg.Payload)), 275 | }} 276 | x.DoProcess(context.Background(), x.Router, exchange) 277 | }) 278 | return err 279 | } 280 | 281 | func (x *Wukongim) Printf(format string, v ...interface{}) { 282 | if x.RuleConfig.Logger != nil { 283 | x.RuleConfig.Logger.Printf(format, v...) 284 | } 285 | } 286 | 287 | func (x *Wukongim) initClient() (*wksdk.Client, error) { 288 | if x.client != nil { 289 | return x.client, nil 290 | } else { 291 | x.Locker.Lock() 292 | defer x.Locker.Unlock() 293 | if x.client != nil { 294 | return x.client, nil 295 | } 296 | x.client = wksdk.NewClient(x.Config.Server, 297 | wksdk.WithConnectTimeout(time.Duration(x.Config.ConnectTimeout)*time.Second), 298 | wksdk.WithProtoVersion(x.Config.ProtoVersion), 299 | wksdk.WithUID(x.Config.UID), 300 | wksdk.WithToken(x.Config.Token), 301 | wksdk.WithPingInterval(time.Duration(x.Config.PingInterval)*time.Second), 302 | wksdk.WithReconnect(x.Config.Reconnect), 303 | ) 304 | err := x.client.Connect() 305 | return x.client, err 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /examples/kafka/kafka_producer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "github.com/rulego/rulego" 22 | "github.com/rulego/rulego/api/types" 23 | "time" 24 | 25 | _ "github.com/rulego/rulego-components/external/kafka" 26 | ) 27 | 28 | //测试x/kafkaProducer组件 29 | func main() { 30 | 31 | config := rulego.NewConfig() 32 | 33 | //初始化规则引擎实例 34 | ruleEngine, err := rulego.New("rule01", []byte(chainJsonFile), rulego.WithConfig(config)) 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | metaData := types.NewMetadata() 40 | metaData.PutValue("key", "test01") 41 | metaData.PutValue("value", "test01_value") 42 | 43 | msg := types.NewMsg(0, "TEST_MSG_TYPE1", types.JSON, metaData, "{\"temperature\":41}") 44 | 45 | ruleEngine.OnMsg(msg, types.WithEndFunc(func(ctx types.RuleContext, msg types.RuleMsg, err error) { 46 | fmt.Println("1.msg处理结果=====") 47 | //得到规则链处理结果 48 | fmt.Println(msg, err) 49 | })) 50 | 51 | time.Sleep(time.Second * 1) 52 | 53 | metaData = types.NewMetadata() 54 | metaData.PutValue("key", "test02") 55 | 56 | msg = types.NewMsg(0, "TEST_MSG_TYPE2", types.JSON, metaData, "{\"temperature\":42}") 57 | 58 | ruleEngine.OnMsg(msg, types.WithEndFunc(func(ctx types.RuleContext, msg types.RuleMsg, err error) { 59 | fmt.Println("2.msg处理结果=====") 60 | //得到规则链处理结果 61 | fmt.Println(msg, err) 62 | })) 63 | 64 | time.Sleep(time.Second * 1) 65 | } 66 | 67 | var chainJsonFile = ` 68 | { 69 | "ruleChain": { 70 | "id":"chain_msg_type_switch", 71 | "name": "测试规则链-msgTypeSwitch", 72 | "root": false, 73 | "debugMode": false 74 | }, 75 | "metadata": { 76 | "nodes": [ 77 | { 78 | "id": "s1", 79 | "type": "msgTypeSwitch", 80 | "name": "过滤", 81 | "debugMode": true 82 | }, 83 | { 84 | "id": "s2", 85 | "type": "log", 86 | "name": "记录日志1", 87 | "debugMode": true, 88 | "configuration": { 89 | "jsScript": "return msgType+':s2--'+JSON.stringify(msg);" 90 | } 91 | }, 92 | { 93 | "id": "s3", 94 | "type": "log", 95 | "name": "记录日志2", 96 | "debugMode": true, 97 | "configuration": { 98 | "jsScript": "return msgType+':s3--'+JSON.stringify(msg);" 99 | } 100 | }, 101 | { 102 | "id": "s5", 103 | "type": "x/kafkaProducer", 104 | "name": "发布到kafka", 105 | "debugMode": true, 106 | "configuration": { 107 | "topic": "device.msg.request", 108 | "brokers": ["localhost:9092"] 109 | } 110 | } 111 | ], 112 | "connections": [ 113 | { 114 | "fromId": "s1", 115 | "toId": "s2", 116 | "type": "TEST_MSG_TYPE1" 117 | }, 118 | { 119 | "fromId": "s1", 120 | "toId": "s3", 121 | "type": "TEST_MSG_TYPE2" 122 | }, 123 | { 124 | "fromId": "s3", 125 | "toId": "s5", 126 | "type": "Success" 127 | } 128 | ] 129 | } 130 | } 131 | ` 132 | -------------------------------------------------------------------------------- /examples/redis/call_redis_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "github.com/rulego/rulego" 22 | "github.com/rulego/rulego/api/types" 23 | "time" 24 | 25 | _ "github.com/rulego/rulego-components/external/redis" 26 | ) 27 | 28 | // 测试x/redisClient组件 29 | func main() { 30 | 31 | config := rulego.NewConfig() 32 | 33 | //初始化规则引擎实例 34 | ruleEngine, err := rulego.New("rule01", []byte(chainJsonFile), rulego.WithConfig(config)) 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | metaData := types.NewMetadata() 40 | metaData.PutValue("key", "test01") 41 | metaData.PutValue("value", "test01_value") 42 | 43 | msg := types.NewMsg(0, "TEST_MSG_TYPE1", types.JSON, metaData, "{\"temperature\":41}") 44 | 45 | ruleEngine.OnMsg(msg, types.WithEndFunc(func(ctx types.RuleContext, msg types.RuleMsg, err error) { 46 | fmt.Println("1.msg处理结果=====") 47 | //得到规则链处理结果 48 | fmt.Println(msg, err) 49 | })) 50 | 51 | time.Sleep(time.Second * 1) 52 | 53 | metaData = types.NewMetadata() 54 | metaData.PutValue("key", "test02") 55 | 56 | msg = types.NewMsg(0, "TEST_MSG_TYPE2", types.JSON, metaData, "{\"temperature\":42}") 57 | 58 | ruleEngine.OnMsg(msg, types.WithEndFunc(func(ctx types.RuleContext, msg types.RuleMsg, err error) { 59 | fmt.Println("2.msg处理结果=====") 60 | //得到规则链处理结果 61 | fmt.Println(msg, err) 62 | })) 63 | 64 | time.Sleep(time.Second * 1) 65 | } 66 | 67 | var chainJsonFile = ` 68 | { 69 | "ruleChain": { 70 | "id":"chain_msg_type_switch", 71 | "name": "测试规则链-msgTypeSwitch", 72 | "root": false, 73 | "debugMode": false 74 | }, 75 | "metadata": { 76 | "nodes": [ 77 | { 78 | "id": "s1", 79 | "type": "msgTypeSwitch", 80 | "name": "过滤", 81 | "debugMode": true 82 | }, 83 | { 84 | "id": "s2", 85 | "type": "log", 86 | "name": "记录日志1", 87 | "debugMode": true, 88 | "configuration": { 89 | "jsScript": "return msgType+':s2--'+JSON.stringify(msg);" 90 | } 91 | }, 92 | { 93 | "id": "s3", 94 | "type": "log", 95 | "name": "记录日志2", 96 | "debugMode": true, 97 | "configuration": { 98 | "jsScript": "return msgType+':s3--'+JSON.stringify(msg);" 99 | } 100 | }, 101 | { 102 | "id": "s5", 103 | "type": "x/redisClient", 104 | "name": "保存到redis", 105 | "debugMode": true, 106 | "configuration": { 107 | "cmd": "SET", 108 | "params": ["${metadata.key}", "${data}"], 109 | "poolSize": 10, 110 | "Server": "127.0.0.1:6379" 111 | } 112 | }, 113 | { 114 | "id": "s6", 115 | "type": "x/redisClient", 116 | "name": "保存到redis", 117 | "debugMode": true, 118 | "configuration": { 119 | "cmd": "SET", 120 | "params": ["${metadata.key}", "${metadata.value}"], 121 | "poolSize": 10, 122 | "Server": "127.0.0.1:6379" 123 | } 124 | } 125 | ], 126 | "connections": [ 127 | { 128 | "fromId": "s1", 129 | "toId": "s2", 130 | "type": "TEST_MSG_TYPE1" 131 | }, 132 | { 133 | "fromId": "s1", 134 | "toId": "s3", 135 | "type": "TEST_MSG_TYPE2" 136 | }, 137 | { 138 | "fromId": "s3", 139 | "toId": "s5", 140 | "type": "Success" 141 | }, 142 | { 143 | "fromId": "s2", 144 | "toId": "s6", 145 | "type": "Success" 146 | } 147 | ] 148 | } 149 | } 150 | ` 151 | -------------------------------------------------------------------------------- /examples/wukongim/wukongim_sender.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | 23 | "github.com/rulego/rulego" 24 | "github.com/rulego/rulego/api/types" 25 | 26 | _ "github.com/rulego/rulego-components/external/wukongim" 27 | ) 28 | 29 | // 测试x/wukongimClient 30 | func main() { 31 | 32 | config := rulego.NewConfig() 33 | 34 | //初始化规则引擎实例 35 | ruleEngine, err := rulego.New("rule01", []byte(chainJsonFile), rulego.WithConfig(config)) 36 | if err != nil { 37 | panic(err) 38 | } 39 | metaData := types.NewMetadata() 40 | metaData.PutValue("channelId", "group1") 41 | metaData.PutValue("channelType", "2") 42 | msg := types.NewMsg(0, "", types.JSON, metaData, `{"orderNo":"1234567890","title":"冰美式不加糖","imgUrl":"https://pic2.zhimg.com/v2-2d6108b9a038c6b6af6648471bb5e0fa_xll.jpg?source=32738c0c","num":1,"price":8,"type":56}`) 43 | 44 | ruleEngine.OnMsg(msg, types.WithEndFunc(func(ctx types.RuleContext, msg types.RuleMsg, err error) { 45 | fmt.Println("msg处理结果=====") 46 | //得到规则链处理结果 47 | fmt.Println(msg, err) 48 | })) 49 | time.Sleep(time.Second * 1) 50 | } 51 | 52 | var chainJsonFile = ` 53 | { 54 | "ruleChain": { 55 | "id": "j-VTV0NZgtgA", 56 | "name": "悟空IM发送测试", 57 | "root": true, 58 | "additionalInfo": { 59 | "description": "", 60 | "layoutX": "670", 61 | "layoutY": "330" 62 | }, 63 | "configuration": {}, 64 | "disabled": false 65 | }, 66 | "metadata": { 67 | "endpoints": [], 68 | "nodes": [ 69 | { 70 | "id": "node_2", 71 | "type": "x/wukongimSender", 72 | "name": "发送节点", 73 | "configuration": { 74 | "server": "tcp://127.0.0.1:5100", 75 | "uID": "test1", 76 | "token": "test1", 77 | "connectTimeout": "50", 78 | "protoVersion": 3, 79 | "pingInterval": "300", 80 | "reconnect": true, 81 | "autoAck": true, 82 | "channelID": "test2", 83 | "channelType": 1, 84 | "redDot": true 85 | }, 86 | "debugMode": true, 87 | "additionalInfo": { 88 | "layoutX": 990, 89 | "layoutY": 330 90 | } 91 | } 92 | ], 93 | "connections": [] 94 | } 95 | } 96 | ` 97 | -------------------------------------------------------------------------------- /external/README.md: -------------------------------------------------------------------------------- 1 | # external 2 | External integration, integrate with third-party systems, such as: calling kafka, database, third-party api, etc. 3 | 4 | ## How to customize components 5 | Implement the `types.Node` interface. 6 | Example of a custom component: 7 | ```go 8 | // Define the Node component 9 | // UpperNode A plugin that converts the message data to uppercase 10 | type UpperNode struct{} 11 | 12 | func (n *UpperNode) Type() string { 13 | return "test/upper" 14 | } 15 | 16 | func (n *UpperNode) New() types.Node { 17 | return &UpperNode{} 18 | } 19 | 20 | func (n *UpperNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 21 | // Do some initialization work 22 | return nil 23 | } 24 | 25 | // Process the message 26 | func (n *UpperNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 27 | msg.Data = strings.ToUpper(msg.Data) 28 | // Send the modified message to the next node 29 | ctx.TellSuccess(msg) 30 | return nil 31 | } 32 | 33 | func (n *UpperNode) Destroy() { 34 | // Do some cleanup work 35 | } 36 | ``` 37 | 38 | ## Usage 39 | 40 | Register the component to the `RuleGo` default registry: 41 | ```go 42 | rulego.Registry.Register(&MyNode{}) 43 | ``` 44 | 45 | Then use your component in the rule chain DSL file: 46 | ```json 47 | { 48 | "ruleChain": { 49 | "id": "rule01", 50 | "name": "Test rule chain" 51 | }, 52 | "metadata": { 53 | "nodes": [ 54 | { 55 | "id": "s1", 56 | "type": "test/upper", 57 | "name": "Name", 58 | "debugMode": true, 59 | "configuration": { 60 | "field1": "Configuration parameters defined by the component", 61 | "....": "..." 62 | } 63 | } 64 | ], 65 | "connections": [ 66 | { 67 | "fromId": "s1", 68 | "toId": "Connect to the next component ID", 69 | "type": "The connection relationship with the component" 70 | } 71 | ] 72 | } 73 | } 74 | ``` -------------------------------------------------------------------------------- /external/README_ZH.md: -------------------------------------------------------------------------------- 1 | # external 2 | 外部集成,和第三方系统进行集成,例如:调用kafka、数据库、第三方api等。 3 | 4 | ## 怎样自定义组件 5 | 实现`types.Node`接口,例子: 6 | ```go 7 | //定义Node组件 8 | //UpperNode A plugin that converts the message data to uppercase 9 | type UpperNode struct{} 10 | 11 | func (n *UpperNode) Type() string { 12 | return "test/upper" 13 | } 14 | func (n *UpperNode) New() types.Node { 15 | return &UpperNode{} 16 | } 17 | func (n *UpperNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 18 | // Do some initialization work 19 | return nil 20 | } 21 | //处理消息 22 | func (n *UpperNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 23 | msg.Data = strings.ToUpper(msg.Data) 24 | // Send the modified message to the next node 25 | ctx.TellSuccess(msg) 26 | return nil 27 | } 28 | 29 | func (n *UpperNode) Destroy() { 30 | // Do some cleanup work 31 | } 32 | ``` 33 | 34 | ## 使用 35 | 36 | 把组件注册到`RuleGo`默认注册器 37 | ```go 38 | rulego.Registry.Register(&MyNode{}) 39 | ``` 40 | 41 | 然后在规则链DSL文件使用您的组件 42 | ```json 43 | { 44 | "ruleChain": { 45 | "id": "rule01", 46 | "name": "测试规则链" 47 | }, 48 | "metadata": { 49 | "nodes": [ 50 | { 51 | "id": "s1", 52 | "type": "test/upper", 53 | "name": "名称", 54 | "debugMode": true, 55 | "configuration": { 56 | "field1": "组件定义的配置参数", 57 | "....": "..." 58 | } 59 | } 60 | ], 61 | "connections": [ 62 | { 63 | "fromId": "s1", 64 | "toId": "连接下一个组件ID", 65 | "type": "与组件的连接关系" 66 | } 67 | ] 68 | } 69 | } 70 | ``` -------------------------------------------------------------------------------- /external/beanstalkd/worker_node.go: -------------------------------------------------------------------------------- 1 | package beanstalkd 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strconv" 7 | "strings" 8 | "time" 9 | 10 | "github.com/beanstalkd/go-beanstalk" 11 | "github.com/rulego/rulego" 12 | "github.com/rulego/rulego/api/types" 13 | "github.com/rulego/rulego/components/base" 14 | "github.com/rulego/rulego/utils/maps" 15 | "github.com/rulego/rulego/utils/str" 16 | ) 17 | 18 | const ( 19 | Delete = "Delete" 20 | Release = "Release" 21 | Bury = "Bury" 22 | KickJob = "KickJob" 23 | Touch = "Touch" 24 | Peek = "Peek" 25 | ReserveJob = "ReserveJob" 26 | StatsJob = "StatsJob" 27 | Stats = "Stats" 28 | ListTubes = "ListTubes" 29 | ) 30 | 31 | // 注册节点 32 | func init() { 33 | _ = rulego.Registry.Register(&WorkerNode{}) 34 | } 35 | 36 | type WorkerMsgParams struct { 37 | Id uint64 38 | Tube string 39 | Pri uint32 40 | Delay time.Duration 41 | Ttr time.Duration 42 | Pause time.Duration 43 | Bound int 44 | } 45 | 46 | // WorkerConfiguration 节点配置 47 | type WorkerConfiguration struct { 48 | // 服务器地址 49 | Server string 50 | // Tube名称 允许使用 ${} 占位符变量 51 | Tube string 52 | // 命令名称,支持Delete Release Bury KickJob Touch Peek ReserveJob StatsJob Stats ListTubes 53 | Cmd string 54 | // JobId 允许使用 ${} 占位符变量 55 | JobId string 56 | // 优先级: pri 允许使用 ${} 占位符变量 57 | Pri string 58 | // 延迟时间: delay 允许使用 ${} 占位符变量 59 | Delay string 60 | } 61 | 62 | // WorkerNode 客户端节点, 63 | // 成功:转向Success链,发送消息执行结果存放在msg.Data 64 | // 失败:转向Failure链 65 | type WorkerNode struct { 66 | base.SharedNode[*beanstalk.Conn] 67 | //节点配置 68 | Config WorkerConfiguration 69 | conn *beanstalk.Conn 70 | tubeTemplate str.Template 71 | jobIdTemplate str.Template 72 | putPriTemplate str.Template 73 | putDelayTemplate str.Template 74 | } 75 | 76 | // Type 返回组件类型 77 | func (x *WorkerNode) Type() string { 78 | return "x/beanstalkdWorker" 79 | } 80 | 81 | // New 默认参数 82 | func (x *WorkerNode) New() types.Node { 83 | return &WorkerNode{Config: WorkerConfiguration{ 84 | Server: "127.0.0.1:11300", 85 | Tube: "default", 86 | Cmd: Stats, 87 | }} 88 | } 89 | 90 | // Init 初始化组件 91 | func (x *WorkerNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 92 | err := maps.Map2Struct(configuration, &x.Config) 93 | if err == nil { 94 | //初始化客户端 95 | err = x.SharedNode.Init(ruleConfig, x.Type(), x.Config.Server, false, func() (*beanstalk.Conn, error) { 96 | return x.initClient() 97 | }) 98 | } 99 | //初始化模板 100 | x.tubeTemplate = str.NewTemplate(x.Config.Tube) 101 | x.putPriTemplate = str.NewTemplate(x.Config.Pri) 102 | x.putDelayTemplate = str.NewTemplate(x.Config.Delay) 103 | x.jobIdTemplate = str.NewTemplate(x.Config.JobId) 104 | return err 105 | } 106 | 107 | // OnMsg 处理消息 108 | func (x *WorkerNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 109 | x.Locker.Lock() 110 | defer x.Locker.Unlock() 111 | var ( 112 | err error 113 | body []byte 114 | tubes []string = make([]string, 0) 115 | data map[string]string = make(map[string]string) 116 | params *WorkerMsgParams 117 | stat map[string]string 118 | ) 119 | params, err = x.getParams(ctx, msg) 120 | // use tube 121 | x.conn, err = x.SharedNode.Get() 122 | if err != nil { 123 | ctx.TellFailure(msg, err) 124 | return 125 | } 126 | x.Printf("conn :%v ", x.conn) 127 | x.conn.Tube.Name = params.Tube 128 | switch x.Config.Cmd { 129 | case Delete: 130 | if params.Id == 0 { 131 | err = errors.New("id is empty") 132 | break 133 | } 134 | err = x.conn.Delete(params.Id) 135 | x.Printf("delete job id:%d tube:%s with err: %s", params.Id, x.conn.Tube.Name, err) 136 | case Release: 137 | if params.Id == 0 { 138 | err = errors.New("id is empty") 139 | break 140 | } 141 | err = x.conn.Release(params.Id, params.Pri, params.Delay) 142 | x.Printf("release job id:%d tube:%s with err: %s", params.Id, x.conn.Tube.Name, err) 143 | case Bury: 144 | if params.Id == 0 { 145 | err = errors.New("id is empty") 146 | break 147 | } 148 | err = x.conn.Bury(params.Id, params.Pri) 149 | x.Printf("bury job id:%d tube:%s with err: %s", params.Id, x.conn.Tube.Name, err) 150 | case KickJob: 151 | if params.Id == 0 { 152 | err = errors.New("id is empty") 153 | break 154 | } 155 | err = x.conn.KickJob(params.Id) 156 | x.Printf("kick job id:%d tube:%s with err: %s", params.Id, x.conn.Tube.Name, err) 157 | case Touch: 158 | if params.Id == 0 { 159 | err = errors.New("id is empty") 160 | break 161 | } 162 | err = x.conn.Touch(params.Id) 163 | x.Printf("touch job id:%d tube:%s with err: %s", params.Id, x.conn.Tube.Name, err) 164 | case Peek: 165 | if params.Id == 0 { 166 | err = errors.New("id is empty") 167 | break 168 | } 169 | body, err = x.conn.Peek(params.Id) 170 | data["body"] = string(body) 171 | x.Printf("peek job id:%d tube:%s with err: %s", params.Id, x.conn.Tube.Name, err) 172 | case ReserveJob: 173 | if params.Id == 0 { 174 | err = errors.New("id is empty") 175 | break 176 | } 177 | body, err = x.conn.ReserveJob(params.Id) 178 | data["body"] = string(body) 179 | x.Printf("reserve job id:%d tube:%s with err: %s", params.Id, x.conn.Tube.Name, err) 180 | case StatsJob: 181 | if params.Id == 0 { 182 | err = errors.New("id is empty") 183 | break 184 | } 185 | data, err = x.conn.StatsJob(params.Id) 186 | x.Printf("stats job id:%d tube:%s with err: %s", params.Id, x.conn.Tube.Name, err) 187 | case Stats: 188 | data, err = x.conn.Stats() 189 | x.Printf("stats :%v with err: %s", data, err) 190 | case ListTubes: 191 | tubes, err = x.conn.ListTubes() 192 | data["tubes"] = strings.Join(tubes, ",") 193 | x.Printf("tubes :%v with err: %s", tubes, err) 194 | default: 195 | err = errors.New("Unknown Command") 196 | } 197 | if err != nil { 198 | ctx.TellFailure(msg, err) 199 | } else { 200 | bytes, err := json.Marshal(data) 201 | if err != nil { 202 | ctx.TellFailure(msg, err) 203 | return 204 | } 205 | msg.SetData(str.ToString(bytes)) 206 | if params.Id > 0 { 207 | stat, err = x.conn.StatsJob(params.Id) 208 | if err == nil { 209 | msg.Metadata.ReplaceAll(stat) 210 | } 211 | 212 | } 213 | ctx.TellSuccess(msg) 214 | } 215 | } 216 | 217 | // getParams 获取参数 218 | func (x *WorkerNode) getParams(ctx types.RuleContext, msg types.RuleMsg) (*WorkerMsgParams, error) { 219 | var ( 220 | err error 221 | id uint64 = 0 222 | tube string = DefaultTube 223 | pri uint32 = DefaultPri 224 | delay time.Duration = DefaultDelay 225 | params = WorkerMsgParams{ 226 | Id: id, 227 | Tube: tube, 228 | Pri: DefaultPri, 229 | Delay: DefaultDelay, 230 | } 231 | ) 232 | evn := base.NodeUtils.GetEvnAndMetadata(ctx, msg) 233 | // 获取tube参数 234 | if !x.tubeTemplate.IsNotVar() { 235 | tube = x.tubeTemplate.Execute(evn) 236 | } else if len(x.Config.Tube) > 0 { 237 | tube = x.Config.Tube 238 | } 239 | // 获取jobId参数 240 | if !x.jobIdTemplate.IsNotVar() { 241 | tmp := x.jobIdTemplate.Execute(evn) 242 | id, err = strconv.ParseUint(tmp, 10, 64) 243 | } 244 | // 获取优先级参数 245 | var ti int 246 | if !x.putPriTemplate.IsNotVar() { 247 | tmp := x.putPriTemplate.Execute(evn) 248 | ti, err = strconv.Atoi(tmp) 249 | pri = uint32(ti) 250 | } else if len(x.Config.Pri) > 0 { 251 | ti, err = strconv.Atoi(x.Config.Pri) 252 | pri = uint32(ti) 253 | } 254 | if err != nil { 255 | return nil, err 256 | } 257 | // 获取延迟参数 258 | if !x.putDelayTemplate.IsNotVar() { 259 | tmp := x.putDelayTemplate.Execute(evn) 260 | delay, err = time.ParseDuration(tmp) 261 | } else if len(x.Config.Delay) > 0 { 262 | delay, err = time.ParseDuration(x.Config.Delay) 263 | } 264 | if err != nil { 265 | return nil, err 266 | } 267 | // 更新参数 268 | params.Id = id 269 | params.Tube = tube 270 | params.Pri = pri 271 | params.Delay = delay 272 | return ¶ms, nil 273 | } 274 | 275 | // Destroy 销毁组件 276 | func (x *WorkerNode) Destroy() { 277 | if x.conn != nil { 278 | _ = x.conn.Close() 279 | } 280 | } 281 | 282 | // Printf 打印日志 283 | func (x *WorkerNode) Printf(format string, v ...interface{}) { 284 | if x.RuleConfig.Logger != nil { 285 | x.RuleConfig.Logger.Printf(format, v...) 286 | } 287 | } 288 | 289 | // 初始化连接 290 | func (x *WorkerNode) initClient() (*beanstalk.Conn, error) { 291 | if x.conn != nil { 292 | return x.conn, nil 293 | } else { 294 | x.Locker.Lock() 295 | defer x.Locker.Unlock() 296 | if x.conn != nil { 297 | return x.conn, nil 298 | } 299 | var err error 300 | x.conn, err = beanstalk.Dial("tcp", x.Config.Server) 301 | x.conn.Tube = *beanstalk.NewTube(x.conn, DefaultTube) 302 | return x.conn, err 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /external/grpc/grpc_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grpc 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "errors" 23 | "github.com/fullstorydev/grpcurl" 24 | "github.com/golang/protobuf/proto" 25 | "github.com/jhump/protoreflect/dynamic" 26 | "github.com/jhump/protoreflect/grpcreflect" 27 | "github.com/rulego/rulego" 28 | "github.com/rulego/rulego/api/types" 29 | "github.com/rulego/rulego/components/base" 30 | "github.com/rulego/rulego/utils/maps" 31 | "github.com/rulego/rulego/utils/str" 32 | "google.golang.org/grpc" 33 | "google.golang.org/grpc/credentials/insecure" 34 | "io" 35 | ) 36 | 37 | func init() { 38 | _ = rulego.Registry.Register(&ClientNode{}) 39 | } 40 | 41 | // SeparatorService grpc service和method 分隔符 42 | const SeparatorService = "/" 43 | 44 | // SeparatorHeader header key:value 分隔符 45 | const SeparatorHeader = ":" 46 | 47 | // ClientConfig 定义 gRPC 客户端配置 48 | type ClientConfig struct { 49 | // Server 服务器地址,格式:host:port 50 | Server string 51 | // Service gRPC 服务名称,允许使用 ${} 占位符变量 52 | Service string 53 | // Method gRPC 方法名称,允许使用 ${} 占位符变量 54 | Method string 55 | // 请求参数 如果空,则使用当前消息负荷。参数使用JSON编码,必须和service/method要求一致。允许使用 ${} 占位符变量 56 | Request string 57 | //Headers 请求头,可以使用 ${metadata.key} 读取元数据中的变量或者使用 ${msg.key} 读取消息负荷中的变量进行替换 58 | Headers map[string]string 59 | } 60 | 61 | // ClientNode gRPC 查询节点 62 | type ClientNode struct { 63 | base.SharedNode[*Client] 64 | Config ClientConfig 65 | client *Client 66 | serviceTemplate str.Template 67 | methodTemplate str.Template 68 | requestTemplate str.Template 69 | headersTemplate map[str.Template]str.Template 70 | hasVar bool 71 | } 72 | 73 | // New 实现 Node 接口,创建新实例 74 | func (x *ClientNode) New() types.Node { 75 | return &ClientNode{ 76 | Config: ClientConfig{ 77 | Server: "127.0.0.1:50051", 78 | Service: "helloworld.Greeter", 79 | Method: "SayHello", 80 | }, 81 | } 82 | } 83 | 84 | // Type 实现 Node 接口,返回组件类型 85 | func (x *ClientNode) Type() string { 86 | return "x/grpcClient" 87 | } 88 | 89 | // Init 初始化 gRPC 客户端 90 | func (x *ClientNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 91 | err := maps.Map2Struct(configuration, &x.Config) 92 | if err != nil { 93 | return err 94 | } 95 | _ = x.SharedNode.Init(ruleConfig, x.Type(), x.Config.Server, ruleConfig.NodeClientInitNow, func() (*Client, error) { 96 | return x.initClient() 97 | }) 98 | x.serviceTemplate = str.NewTemplate(x.Config.Service) 99 | x.methodTemplate = str.NewTemplate(x.Config.Method) 100 | x.requestTemplate = str.NewTemplate(x.Config.Request) 101 | if !x.serviceTemplate.IsNotVar() || !x.methodTemplate.IsNotVar() || !x.requestTemplate.IsNotVar() { 102 | x.hasVar = true 103 | } 104 | var headerTemplates = make(map[str.Template]str.Template) 105 | for key, value := range x.Config.Headers { 106 | keyTmpl := str.NewTemplate(key) 107 | valueTmpl := str.NewTemplate(value) 108 | headerTemplates[keyTmpl] = valueTmpl 109 | if !keyTmpl.IsNotVar() || !valueTmpl.IsNotVar() { 110 | x.hasVar = true 111 | } 112 | } 113 | x.headersTemplate = headerTemplates 114 | return nil 115 | } 116 | 117 | // OnMsg 实现 Node 接口,处理消息 118 | func (x *ClientNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 119 | client, err := x.SharedNode.Get() 120 | if err != nil { 121 | ctx.TellFailure(msg, err) 122 | return 123 | } 124 | descSource := grpcurl.DescriptorSourceFromServer(context.Background(), client.client) 125 | 126 | var evn map[string]interface{} 127 | if x.hasVar { 128 | evn = base.NodeUtils.GetEvnAndMetadata(ctx, msg) 129 | } 130 | service := x.serviceTemplate.Execute(evn) 131 | method := x.methodTemplate.Execute(evn) 132 | request := x.requestTemplate.Execute(evn) 133 | if request == "" { 134 | request = msg.GetData() 135 | } 136 | serviceAndMethod := service + SeparatorService + method 137 | var responseBuffer bytes.Buffer 138 | handler := &grpcurl.DefaultEventHandler{ 139 | Out: &responseBuffer, 140 | Formatter: func(message proto.Message) (string, error) { 141 | protoMessage, ok := message.(*dynamic.Message) 142 | if !ok { 143 | return "", errors.New("invalid message type") 144 | } 145 | 146 | if v, err := protoMessage.MarshalJSON(); err != nil { 147 | return "", err 148 | } else { 149 | return string(v), nil 150 | } 151 | }, 152 | } 153 | // 实现RequestSupplier函数 154 | requestDataSupplier := func(message proto.Message) error { 155 | // 将请求数据填充到protobuf消息中 156 | protoMessage, ok := message.(*dynamic.Message) 157 | if !ok { 158 | return errors.New("invalid message type") 159 | } 160 | protoMessage.Reset() 161 | if err := protoMessage.UnmarshalJSON([]byte(request)); err != nil { 162 | return err 163 | } 164 | // 如果是一次性请求,返回io.EOF表示没有更多请求数据 165 | return io.EOF 166 | } 167 | var headers []string 168 | //设置header 169 | for key, value := range x.headersTemplate { 170 | headers = append(headers, key.Execute(evn)+SeparatorHeader+value.Execute(evn)) 171 | } 172 | err = grpcurl.InvokeRPC(context.Background(), descSource, client.conn, serviceAndMethod, headers, handler, requestDataSupplier) 173 | if err != nil { 174 | ctx.TellFailure(msg, err) 175 | return 176 | } else { 177 | msg.SetData(responseBuffer.String()) 178 | ctx.TellSuccess(msg) 179 | } 180 | } 181 | 182 | // Destroy 清理资源 183 | func (x *ClientNode) Destroy() { 184 | if x.client != nil && x.client.conn != nil { 185 | _ = x.client.conn.Close() 186 | x.client = nil 187 | } 188 | } 189 | 190 | func (x *ClientNode) initClient() (*Client, error) { 191 | if x.client != nil { 192 | return x.client, nil 193 | } else { 194 | x.Locker.Lock() 195 | defer x.Locker.Unlock() 196 | if x.client != nil { 197 | return x.client, nil 198 | } 199 | var err error 200 | conn, err := grpc.Dial(x.Config.Server, grpc.WithTransportCredentials(insecure.NewCredentials())) 201 | if err != nil { 202 | return nil, err 203 | } 204 | rc := grpcreflect.NewClientAuto(context.Background(), conn) 205 | x.client = &Client{ 206 | client: rc, 207 | conn: conn, 208 | } 209 | return x.client, err 210 | } 211 | } 212 | 213 | type Client struct { 214 | client *grpcreflect.Client 215 | conn *grpc.ClientConn 216 | } 217 | -------------------------------------------------------------------------------- /external/grpc/grpc_client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package grpc 18 | 19 | import ( 20 | "github.com/rulego/rulego/api/types" 21 | "github.com/rulego/rulego/test" 22 | "github.com/rulego/rulego/test/assert" 23 | "testing" 24 | "time" 25 | ) 26 | 27 | func TestClientNode(t *testing.T) { 28 | Registry := &types.SafeComponentSlice{} 29 | Registry.Add(&ClientNode{}) 30 | var targetNodeType = "x/grpcClient" 31 | 32 | t.Run("NewNode", func(t *testing.T) { 33 | test.NodeNew(t, targetNodeType, &ClientNode{}, types.Configuration{ 34 | "server": "127.0.0.1:50051", 35 | "service": "helloworld.Greeter", 36 | "method": "SayHello", 37 | }, Registry) 38 | }) 39 | 40 | t.Run("InitNode", func(t *testing.T) { 41 | test.NodeInit(t, targetNodeType, types.Configuration{ 42 | "server": "127.0.0.1:50051", 43 | "service": "helloworld.Greeter", 44 | "method": "SayHello", 45 | "request": `{"name2": "helloWorld2"}`, 46 | }, types.Configuration{ 47 | "server": "127.0.0.1:50051", 48 | "service": "helloworld.Greeter", 49 | "method": "SayHello", 50 | "request": `{"name2": "helloWorld2"}`, 51 | }, Registry) 52 | }) 53 | 54 | t.Run("OnMsg", func(t *testing.T) { 55 | node1, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 56 | "server": "127.0.0.1:50051", 57 | "service": "helloworld.Greeter", 58 | "method": "SayHello", 59 | "request": `{"name": "lulu","groupName":"app01"}`, 60 | "headers": map[string]string{ 61 | "key1": "value1", 62 | }, 63 | }, Registry) 64 | assert.Nil(t, err) 65 | 66 | node2, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 67 | "server": "127.0.0.1:50051", 68 | "service": "helloworld.Greeter", 69 | "method": "SayHello", 70 | "headers": map[string]string{ 71 | "key1": "${fromId}", 72 | }, 73 | }, Registry) 74 | assert.Nil(t, err) 75 | 76 | node3, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 77 | "server": "127.0.0.1:50051", 78 | "service": "helloworld.Greeter", 79 | "method": "${metadata.method}", 80 | }, Registry) 81 | assert.Nil(t, err) 82 | 83 | metaData := types.BuildMetadata(make(map[string]string)) 84 | metaData.PutValue("service", "helloworld.Greeter") 85 | metaData.PutValue("method", "SayHello") 86 | metaData.PutValue("fromId", "aa") 87 | msgList := []test.Msg{ 88 | { 89 | MetaData: metaData, 90 | MsgType: "ACTIVITY_EVENT1", 91 | Data: "{\"name\": \"lala\"}", 92 | }, 93 | } 94 | var nodeList = []test.NodeAndCallback{ 95 | { 96 | Node: node1, 97 | MsgList: msgList, 98 | Callback: func(msg types.RuleMsg, relationType string, err error) { 99 | assert.Equal(t, "{\"message\":\"Hello lulu\",\"groupName\":\"app01\"}\n", msg.Data) 100 | assert.Equal(t, types.Success, relationType) 101 | }, 102 | }, 103 | { 104 | Node: node2, 105 | MsgList: msgList, 106 | Callback: func(msg types.RuleMsg, relationType string, err error) { 107 | assert.Equal(t, "{\"message\":\"Hello lala\"}\n", msg.Data) 108 | assert.Equal(t, types.Success, relationType) 109 | }, 110 | }, 111 | { 112 | Node: node3, 113 | MsgList: msgList, 114 | Callback: func(msg types.RuleMsg, relationType string, err error) { 115 | assert.Equal(t, "{\"message\":\"Hello lala\"}\n", msg.Data) 116 | assert.Equal(t, types.Success, relationType) 117 | }, 118 | }, 119 | } 120 | for _, item := range nodeList { 121 | test.NodeOnMsgWithChildren(t, item.Node, item.MsgList, item.ChildrenNodes, item.Callback) 122 | } 123 | time.Sleep(time.Second * 3) 124 | }) 125 | } 126 | -------------------------------------------------------------------------------- /external/grpc/testdata/helloworld/helloworld.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: helloworld.proto 3 | 4 | package helloworld 5 | 6 | import ( 7 | fmt "fmt" 8 | proto "github.com/golang/protobuf/proto" 9 | math "math" 10 | ) 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the proto package it is being compiled against. 19 | // A compilation error at this line likely means your copy of the 20 | // proto package needs to be updated. 21 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 22 | 23 | type HelloRequest struct { 24 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 25 | GroupName string `protobuf:"bytes,2,opt,name=groupName,proto3" json:"groupName,omitempty"` 26 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 27 | XXX_unrecognized []byte `json:"-"` 28 | XXX_sizecache int32 `json:"-"` 29 | } 30 | 31 | func (m *HelloRequest) Reset() { *m = HelloRequest{} } 32 | func (m *HelloRequest) String() string { return proto.CompactTextString(m) } 33 | func (*HelloRequest) ProtoMessage() {} 34 | func (*HelloRequest) Descriptor() ([]byte, []int) { 35 | return fileDescriptor_17b8c58d586b62f2, []int{0} 36 | } 37 | 38 | func (m *HelloRequest) XXX_Unmarshal(b []byte) error { 39 | return xxx_messageInfo_HelloRequest.Unmarshal(m, b) 40 | } 41 | func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 42 | return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic) 43 | } 44 | func (m *HelloRequest) XXX_Merge(src proto.Message) { 45 | xxx_messageInfo_HelloRequest.Merge(m, src) 46 | } 47 | func (m *HelloRequest) XXX_Size() int { 48 | return xxx_messageInfo_HelloRequest.Size(m) 49 | } 50 | func (m *HelloRequest) XXX_DiscardUnknown() { 51 | xxx_messageInfo_HelloRequest.DiscardUnknown(m) 52 | } 53 | 54 | var xxx_messageInfo_HelloRequest proto.InternalMessageInfo 55 | 56 | func (m *HelloRequest) GetName() string { 57 | if m != nil { 58 | return m.Name 59 | } 60 | return "" 61 | } 62 | 63 | func (m *HelloRequest) GetGroupName() string { 64 | if m != nil { 65 | return m.GroupName 66 | } 67 | return "" 68 | } 69 | 70 | type HelloReply struct { 71 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` 72 | GroupName string `protobuf:"bytes,2,opt,name=groupName,proto3" json:"groupName,omitempty"` 73 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 74 | XXX_unrecognized []byte `json:"-"` 75 | XXX_sizecache int32 `json:"-"` 76 | } 77 | 78 | func (m *HelloReply) Reset() { *m = HelloReply{} } 79 | func (m *HelloReply) String() string { return proto.CompactTextString(m) } 80 | func (*HelloReply) ProtoMessage() {} 81 | func (*HelloReply) Descriptor() ([]byte, []int) { 82 | return fileDescriptor_17b8c58d586b62f2, []int{1} 83 | } 84 | 85 | func (m *HelloReply) XXX_Unmarshal(b []byte) error { 86 | return xxx_messageInfo_HelloReply.Unmarshal(m, b) 87 | } 88 | func (m *HelloReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 89 | return xxx_messageInfo_HelloReply.Marshal(b, m, deterministic) 90 | } 91 | func (m *HelloReply) XXX_Merge(src proto.Message) { 92 | xxx_messageInfo_HelloReply.Merge(m, src) 93 | } 94 | func (m *HelloReply) XXX_Size() int { 95 | return xxx_messageInfo_HelloReply.Size(m) 96 | } 97 | func (m *HelloReply) XXX_DiscardUnknown() { 98 | xxx_messageInfo_HelloReply.DiscardUnknown(m) 99 | } 100 | 101 | var xxx_messageInfo_HelloReply proto.InternalMessageInfo 102 | 103 | func (m *HelloReply) GetMessage() string { 104 | if m != nil { 105 | return m.Message 106 | } 107 | return "" 108 | } 109 | 110 | func (m *HelloReply) GetGroupName() string { 111 | if m != nil { 112 | return m.GroupName 113 | } 114 | return "" 115 | } 116 | 117 | func init() { 118 | proto.RegisterType((*HelloRequest)(nil), "helloworld.HelloRequest") 119 | proto.RegisterType((*HelloReply)(nil), "helloworld.HelloReply") 120 | } 121 | 122 | func init() { 123 | proto.RegisterFile("helloworld.proto", fileDescriptor_17b8c58d586b62f2) 124 | } 125 | 126 | var fileDescriptor_17b8c58d586b62f2 = []byte{ 127 | // 169 bytes of a gzipped FileDescriptorProto 128 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0xc8, 0x48, 0xcd, 0xc9, 129 | 0xc9, 0x2f, 0xcf, 0x2f, 0xca, 0x49, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x42, 0x88, 130 | 0x28, 0x39, 0x70, 0xf1, 0x78, 0x80, 0x78, 0x41, 0xa9, 0x85, 0xa5, 0xa9, 0xc5, 0x25, 0x42, 0x42, 131 | 0x5c, 0x2c, 0x79, 0x89, 0xb9, 0xa9, 0x12, 0x8c, 0x0a, 0x8c, 0x1a, 0x9c, 0x41, 0x60, 0xb6, 0x90, 132 | 0x0c, 0x17, 0x67, 0x7a, 0x51, 0x7e, 0x69, 0x81, 0x1f, 0x48, 0x82, 0x09, 0x2c, 0x81, 0x10, 0x50, 133 | 0x72, 0xe1, 0xe2, 0x82, 0x9a, 0x50, 0x90, 0x53, 0x29, 0x24, 0xc1, 0xc5, 0x9e, 0x9b, 0x5a, 0x5c, 134 | 0x9c, 0x98, 0x0e, 0x33, 0x02, 0xc6, 0xc5, 0x6f, 0x8a, 0x91, 0x27, 0x17, 0xbb, 0x7b, 0x51, 0x6a, 135 | 0x6a, 0x49, 0x6a, 0x91, 0x90, 0x1d, 0x17, 0x47, 0x70, 0x62, 0x25, 0xd8, 0x4c, 0x21, 0x09, 0x3d, 136 | 0x24, 0xd7, 0x23, 0x3b, 0x54, 0x4a, 0x0c, 0x8b, 0x4c, 0x41, 0x4e, 0xa5, 0x12, 0x83, 0x13, 0x5f, 137 | 0x14, 0x8f, 0x9e, 0x35, 0x42, 0x32, 0x89, 0x0d, 0xec, 0x6b, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 138 | 0xff, 0x0e, 0xaf, 0xae, 0x0e, 0x09, 0x01, 0x00, 0x00, 139 | } 140 | -------------------------------------------------------------------------------- /external/grpc/testdata/helloworld/helloworld.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package helloworld; 4 | 5 | option go_package = ".;helloworld"; 6 | 7 | service Greeter { 8 | rpc SayHello (HelloRequest) returns (HelloReply) {} 9 | } 10 | 11 | message HelloRequest { 12 | string name = 1; 13 | string groupName = 2; 14 | } 15 | 16 | message HelloReply { 17 | string message = 1; 18 | string groupName= 2; 19 | } -------------------------------------------------------------------------------- /external/grpc/testdata/helloworld/helloworld_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.2.0 4 | // - protoc v3.21.1 5 | // source: helloworld.proto 6 | 7 | package helloworld 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | // GreeterClient is the client API for Greeter service. 22 | // 23 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 24 | type GreeterClient interface { 25 | SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) 26 | } 27 | 28 | type greeterClient struct { 29 | cc grpc.ClientConnInterface 30 | } 31 | 32 | func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient { 33 | return &greeterClient{cc} 34 | } 35 | 36 | func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { 37 | out := new(HelloReply) 38 | err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return out, nil 43 | } 44 | 45 | // GreeterServer is the server API for Greeter service. 46 | // All implementations must embed UnimplementedGreeterServer 47 | // for forward compatibility 48 | type GreeterServer interface { 49 | SayHello(context.Context, *HelloRequest) (*HelloReply, error) 50 | mustEmbedUnimplementedGreeterServer() 51 | } 52 | 53 | // UnimplementedGreeterServer must be embedded to have forward compatible implementations. 54 | type UnimplementedGreeterServer struct { 55 | } 56 | 57 | func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) { 58 | return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") 59 | } 60 | func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {} 61 | 62 | // UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service. 63 | // Use of this interface is not recommended, as added methods to GreeterServer will 64 | // result in compilation errors. 65 | type UnsafeGreeterServer interface { 66 | mustEmbedUnimplementedGreeterServer() 67 | } 68 | 69 | func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) { 70 | s.RegisterService(&Greeter_ServiceDesc, srv) 71 | } 72 | 73 | func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 74 | in := new(HelloRequest) 75 | if err := dec(in); err != nil { 76 | return nil, err 77 | } 78 | if interceptor == nil { 79 | return srv.(GreeterServer).SayHello(ctx, in) 80 | } 81 | info := &grpc.UnaryServerInfo{ 82 | Server: srv, 83 | FullMethod: "/helloworld.Greeter/SayHello", 84 | } 85 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 86 | return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) 87 | } 88 | return interceptor(ctx, in, info, handler) 89 | } 90 | 91 | // Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service. 92 | // It's only intended for direct use with grpc.RegisterService, 93 | // and not to be introspected or modified (even as a copy) 94 | var Greeter_ServiceDesc = grpc.ServiceDesc{ 95 | ServiceName: "helloworld.Greeter", 96 | HandlerType: (*GreeterServer)(nil), 97 | Methods: []grpc.MethodDesc{ 98 | { 99 | MethodName: "SayHello", 100 | Handler: _Greeter_SayHello_Handler, 101 | }, 102 | }, 103 | Streams: []grpc.StreamDesc{}, 104 | Metadata: "helloworld.proto", 105 | } 106 | -------------------------------------------------------------------------------- /external/grpc/testdata/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | pb "github.com/rulego/rulego-components/external/grpc/testdata/helloworld" 22 | "google.golang.org/grpc" 23 | "google.golang.org/grpc/metadata" 24 | "google.golang.org/grpc/reflection" 25 | "log" 26 | "net" 27 | ) 28 | 29 | // server 是 Greeter 服务的服务器实现 30 | type server struct { 31 | pb.UnimplementedGreeterServer 32 | } 33 | 34 | // SayHello 实现 Greeter 服务的 SayHello 方法 35 | func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) { 36 | // 从上下文中获取元数据 37 | md, ok := metadata.FromIncomingContext(ctx) 38 | if ok { 39 | // 打印所有头部信息 40 | for key, values := range md { 41 | log.Printf("Header %s: %v", key, values) 42 | } 43 | } 44 | log.Printf("Received: %v,%v", in.GetName(), in.GroupName) 45 | return &pb.HelloReply{Message: "Hello " + in.GetName(), GroupName: in.GroupName}, nil 46 | } 47 | 48 | func main() { 49 | listen, err := net.Listen("tcp", ":50051") 50 | if err != nil { 51 | log.Fatalf("failed to listen: %v", err) 52 | } 53 | s := grpc.NewServer() 54 | pb.RegisterGreeterServer(s, &server{}) 55 | // 注册 gRPC 反射服务 56 | reflection.Register(s) 57 | if err := s.Serve(listen); err != nil { 58 | log.Fatalf("failed to serve: %v", err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /external/kafka/kafka_producer.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package kafka 18 | 19 | import ( 20 | "errors" 21 | "github.com/IBM/sarama" 22 | "github.com/rulego/rulego" 23 | "github.com/rulego/rulego/api/types" 24 | "github.com/rulego/rulego/components/base" 25 | "github.com/rulego/rulego/utils/maps" 26 | "github.com/rulego/rulego/utils/str" 27 | "strconv" 28 | "strings" 29 | ) 30 | 31 | const ( 32 | KeyPartition = "partition" 33 | KeOffset = "offset" 34 | ) 35 | 36 | // 注册节点 37 | func init() { 38 | _ = rulego.Registry.Register(&ProducerNode{}) 39 | } 40 | 41 | // NodeConfiguration 节点配置 42 | type NodeConfiguration struct { 43 | // kafka服务器地址列表,多个与逗号隔开 44 | Server string 45 | // Topic 发布主题,可以使用 ${metadata.key} 读取元数据中的变量或者使用 ${msg.key} 读取消息负荷中的变量进行替换 46 | Topic string 47 | // Key 分区键,可以使用 ${metadata.key} 读取元数据中的变量或者使用 ${msg.key} 读取消息负荷中的变量进行替换 48 | Key string 49 | //Partition 分区编号 50 | Partition int32 51 | } 52 | 53 | type ProducerNode struct { 54 | base.SharedNode[sarama.SyncProducer] 55 | Config NodeConfiguration 56 | client sarama.SyncProducer 57 | // brokers kafka服务器地址列表 58 | brokers []string 59 | //topic 模板 60 | topicTemplate str.Template 61 | //key 模板 62 | keyTemplate str.Template 63 | } 64 | 65 | // Type 返回组件类型 66 | func (x *ProducerNode) Type() string { 67 | return "x/kafkaProducer" 68 | } 69 | 70 | func (x *ProducerNode) New() types.Node { 71 | return &ProducerNode{ 72 | Config: NodeConfiguration{ 73 | Server: "127.0.0.1:9092", 74 | Partition: 0, 75 | }, 76 | } 77 | } 78 | 79 | // Init 初始化组件 80 | func (x *ProducerNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 81 | err := maps.Map2Struct(configuration, &x.Config) 82 | if err == nil { 83 | x.brokers = x.getBrokerFromOldVersion(configuration) 84 | if len(x.brokers) == 0 && x.Config.Server != "" { 85 | x.brokers = strings.Split(x.Config.Server, ",") 86 | } 87 | if len(x.brokers) == 0 { 88 | return errors.New("brokers is empty") 89 | } 90 | _ = x.SharedNode.Init(ruleConfig, x.Type(), x.brokers[0], ruleConfig.NodeClientInitNow, func() (sarama.SyncProducer, error) { 91 | return x.initClient() 92 | }) 93 | 94 | x.topicTemplate = str.NewTemplate(x.Config.Topic) 95 | x.keyTemplate = str.NewTemplate(x.Config.Key) 96 | } 97 | return err 98 | } 99 | 100 | // OnMsg 处理消息 101 | func (x *ProducerNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 102 | topic := x.Config.Topic 103 | key := x.Config.Key 104 | if !x.topicTemplate.IsNotVar() || !x.keyTemplate.IsNotVar() { 105 | evn := base.NodeUtils.GetEvnAndMetadata(ctx, msg) 106 | topic = str.ExecuteTemplate(topic, evn) 107 | key = str.ExecuteTemplate(key, evn) 108 | } 109 | 110 | client, err := x.SharedNode.Get() 111 | if err != nil { 112 | ctx.TellFailure(msg, err) 113 | return 114 | } 115 | message := &sarama.ProducerMessage{ 116 | Topic: topic, 117 | Partition: x.Config.Partition, 118 | Key: sarama.StringEncoder(key), 119 | Value: sarama.StringEncoder(msg.GetData()), 120 | } 121 | partition, offset, err := client.SendMessage(message) 122 | if err != nil { 123 | ctx.TellFailure(msg, err) 124 | } else { 125 | msg.Metadata.PutValue(KeyPartition, strconv.Itoa(int(partition))) 126 | msg.Metadata.PutValue(KeOffset, strconv.Itoa(int(offset))) 127 | ctx.TellSuccess(msg) 128 | } 129 | } 130 | 131 | // Destroy 销毁组件 132 | func (x *ProducerNode) Destroy() { 133 | if x.client != nil { 134 | _ = x.client.Close() 135 | } 136 | } 137 | 138 | func (x *ProducerNode) getBrokerFromOldVersion(configuration types.Configuration) []string { 139 | if v, ok := configuration["brokers"]; ok { 140 | return v.([]string) 141 | } else { 142 | return nil 143 | } 144 | } 145 | 146 | func (x *ProducerNode) initClient() (sarama.SyncProducer, error) { 147 | if x.client != nil { 148 | return x.client, nil 149 | } else { 150 | x.Locker.Lock() 151 | defer x.Locker.Unlock() 152 | if x.client != nil { 153 | return x.client, nil 154 | } 155 | var err error 156 | config := sarama.NewConfig() 157 | config.Producer.Return.Successes = true // 同步模式需要设置这个参数为true 158 | x.client, err = sarama.NewSyncProducer(x.brokers, config) 159 | return x.client, err 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /external/kafka/kafka_producer_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package kafka 18 | 19 | import ( 20 | "github.com/rulego/rulego/api/types" 21 | "github.com/rulego/rulego/test" 22 | "github.com/rulego/rulego/test/assert" 23 | "testing" 24 | "time" 25 | ) 26 | 27 | func TestKafkaProducer(t *testing.T) { 28 | Registry := &types.SafeComponentSlice{} 29 | Registry.Add(&ProducerNode{}) 30 | var targetNodeType = "x/kafkaProducer" 31 | 32 | t.Run("InitNode", func(t *testing.T) { 33 | _, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 34 | "server": "", 35 | }, Registry) 36 | assert.Equal(t, "brokers is empty", err.Error()) 37 | 38 | node, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 39 | "server": "localhost:9092", 40 | }, Registry) 41 | assert.Equal(t, "localhost:9092", node.(*ProducerNode).brokers[0]) 42 | 43 | node, err = test.CreateAndInitNode(targetNodeType, types.Configuration{ 44 | "server": "localhost:9092,localhost:9093", 45 | "topic": "device/msg", 46 | "key": "aa", 47 | "partition": 1, 48 | }, Registry) 49 | assert.Equal(t, "localhost:9092", node.(*ProducerNode).brokers[0]) 50 | assert.Equal(t, "localhost:9093", node.(*ProducerNode).brokers[1]) 51 | assert.Equal(t, "device/msg", node.(*ProducerNode).Config.Topic) 52 | assert.Equal(t, "aa", node.(*ProducerNode).Config.Key) 53 | assert.Equal(t, int32(1), node.(*ProducerNode).Config.Partition) 54 | 55 | node, err = test.CreateAndInitNode(targetNodeType, types.Configuration{ 56 | "brokers": []string{"localhost:9092", "localhost:9093"}, 57 | }, Registry) 58 | assert.Equal(t, "localhost:9092", node.(*ProducerNode).brokers[0]) 59 | assert.Equal(t, "localhost:9093", node.(*ProducerNode).brokers[1]) 60 | }) 61 | 62 | } 63 | func TestKafkaProducerNodeOnMsg(t *testing.T) { 64 | var node ProducerNode 65 | var configuration = make(types.Configuration) 66 | configuration["topic"] = "device.msg.request" 67 | configuration["key"] = "${metadata.id}" 68 | configuration["server"] = "localhost:9092" 69 | config := types.NewConfig() 70 | err := node.Init(config, configuration) 71 | if err != nil { 72 | t.Errorf("err=%s", err) 73 | } 74 | ctx := test.NewRuleContext(config, func(msg types.RuleMsg, relationType string, err error) { 75 | assert.Equal(t, types.Success, relationType) 76 | // 检查发布结果是否正确 77 | assert.Equal(t, "0", msg.Metadata.GetValue("partition")) 78 | }) 79 | metaData := types.NewMetadata() 80 | // 在元数据中添加发布键 81 | metaData.PutValue("id", "1") 82 | msg := ctx.NewMsg("TEST_MSG_TYPE_AA", metaData, "{\"test\":\"AA\"}") 83 | node.OnMsg(ctx, msg) 84 | 85 | time.Sleep(time.Millisecond * 20) 86 | node.Destroy() 87 | } 88 | -------------------------------------------------------------------------------- /external/nats/nats_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package nats 18 | 19 | import ( 20 | "github.com/nats-io/nats.go" 21 | "github.com/rulego/rulego" 22 | "github.com/rulego/rulego/api/types" 23 | "github.com/rulego/rulego/components/base" 24 | "github.com/rulego/rulego/utils/maps" 25 | "github.com/rulego/rulego/utils/str" 26 | ) 27 | 28 | func init() { 29 | _ = rulego.Registry.Register(&ClientNode{}) 30 | } 31 | 32 | type ClientNodeConfiguration struct { 33 | // NATS服务器地址 34 | Server string 35 | // NATS用户名 36 | Username string 37 | // NATS密码 38 | Password string 39 | // 发布主题 40 | Topic string 41 | } 42 | 43 | type ClientNode struct { 44 | base.SharedNode[*nats.Conn] 45 | // 节点配置 46 | Config ClientNodeConfiguration 47 | client *nats.Conn 48 | // 是否正在连接NATS服务器 49 | connecting int32 50 | //topic 模板 51 | topicTemplate str.Template 52 | } 53 | 54 | // Type 组件类型 55 | func (x *ClientNode) Type() string { 56 | return "x/natsClient" 57 | } 58 | 59 | func (x *ClientNode) New() types.Node { 60 | return &ClientNode{Config: ClientNodeConfiguration{ 61 | Topic: "/device/msg", 62 | Server: "nats://127.0.0.1:4222", 63 | }} 64 | } 65 | 66 | // Init 初始化 67 | func (x *ClientNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 68 | err := maps.Map2Struct(configuration, &x.Config) 69 | if err == nil { 70 | _ = x.SharedNode.Init(ruleConfig, x.Type(), x.Config.Server, ruleConfig.NodeClientInitNow, func() (*nats.Conn, error) { 71 | return x.initClient() 72 | }) 73 | x.topicTemplate = str.NewTemplate(x.Config.Topic) 74 | } 75 | return err 76 | } 77 | 78 | // OnMsg 处理消息 79 | func (x *ClientNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 80 | topic := x.topicTemplate.ExecuteFn(func() map[string]any { 81 | return base.NodeUtils.GetEvnAndMetadata(ctx, msg) 82 | }) 83 | client, err := x.SharedNode.Get() 84 | if err != nil { 85 | ctx.TellFailure(msg, err) 86 | return 87 | } 88 | if err := client.Publish(topic, []byte(msg.GetData())); err != nil { 89 | ctx.TellFailure(msg, err) 90 | } else { 91 | ctx.TellSuccess(msg) 92 | } 93 | } 94 | 95 | // Destroy 销毁 96 | func (x *ClientNode) Destroy() { 97 | if x.client != nil { 98 | x.client.Close() 99 | } 100 | } 101 | 102 | func (x *ClientNode) initClient() (*nats.Conn, error) { 103 | if x.client != nil { 104 | return x.client, nil 105 | } else { 106 | x.Locker.Lock() 107 | defer x.Locker.Unlock() 108 | if x.client != nil { 109 | return x.client, nil 110 | } 111 | var err error 112 | x.client, err = nats.Connect(x.Config.Server, nats.UserInfo(x.Config.Username, x.Config.Password)) 113 | return x.client, err 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /external/nats/nats_client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package nats 18 | 19 | import ( 20 | "github.com/rulego/rulego/api/types" 21 | "github.com/rulego/rulego/test" 22 | "github.com/rulego/rulego/test/assert" 23 | "testing" 24 | "time" 25 | ) 26 | 27 | func TestClientNode(t *testing.T) { 28 | Registry := &types.SafeComponentSlice{} 29 | Registry.Add(&ClientNode{}) 30 | var targetNodeType = "x/natsClient" 31 | 32 | t.Run("NewNode", func(t *testing.T) { 33 | test.NodeNew(t, targetNodeType, &ClientNode{}, types.Configuration{ 34 | "topic": "/device/msg", 35 | "server": "nats://127.0.0.1:4222", 36 | }, Registry) 37 | }) 38 | 39 | t.Run("InitNode", func(t *testing.T) { 40 | test.NodeInit(t, targetNodeType, types.Configuration{ 41 | "topic": "/device/msg", 42 | "server": "nats://127.0.0.1:4222", 43 | }, types.Configuration{ 44 | "topic": "/device/msg", 45 | "server": "nats://127.0.0.1:4222", 46 | }, Registry) 47 | }) 48 | 49 | t.Run("OnMsg", func(t *testing.T) { 50 | node, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 51 | "topic": "/device/msg", 52 | "server": "nats://127.0.0.1:4222", 53 | }, Registry) 54 | assert.Nil(t, err) 55 | 56 | metaData := types.BuildMetadata(make(map[string]string)) 57 | metaData.PutValue("productType", "test") 58 | msgList := []test.Msg{ 59 | { 60 | MetaData: metaData, 61 | MsgType: "ACTIVITY_EVENT1", 62 | Data: "AA", 63 | }, 64 | { 65 | MetaData: metaData, 66 | MsgType: "ACTIVITY_EVENT2", 67 | Data: "{\"temperature\":60}", 68 | }, 69 | } 70 | 71 | var nodeList = []test.NodeAndCallback{ 72 | { 73 | Node: node, 74 | MsgList: msgList, 75 | Callback: func(msg types.RuleMsg, relationType string, err error) { 76 | assert.Equal(t, types.Success, relationType) 77 | }, 78 | }, 79 | } 80 | for _, item := range nodeList { 81 | test.NodeOnMsgWithChildren(t, item.Node, item.MsgList, item.ChildrenNodes, item.Callback) 82 | } 83 | time.Sleep(time.Second * 10) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /external/opengemini/query.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package opengemini 18 | 19 | import ( 20 | "errors" 21 | "github.com/openGemini/opengemini-client-go/opengemini" 22 | "github.com/rulego/rulego" 23 | "github.com/rulego/rulego/api/types" 24 | "github.com/rulego/rulego/components/base" 25 | "github.com/rulego/rulego/utils/maps" 26 | "github.com/rulego/rulego/utils/str" 27 | ) 28 | 29 | func init() { 30 | _ = rulego.Registry.Register(&QueryNode{}) 31 | } 32 | 33 | // QueryConfig 定义 OpenGemini 客户端配置 34 | type QueryConfig struct { 35 | // Server 服务器地址,格式:host:port,多个地址用逗号分隔 36 | Server string 37 | // Database 数据库,允许使用 ${} 占位符变量 38 | Database string 39 | // Username 用户名 40 | Username string 41 | // Password 密码 42 | Password string 43 | // Token 如果token 不为空,则使用token认证,否则使用用户名密码认证 44 | Token string 45 | // 查询语句,允许使用 ${} 占位符变量 46 | Command string 47 | } 48 | 49 | // QueryNode opengemini 查询节点 50 | type QueryNode struct { 51 | *WriteNode 52 | Config QueryConfig 53 | commandTemplate str.Template 54 | } 55 | 56 | // New 实现 Node 接口,创建新实例 57 | func (x *QueryNode) New() types.Node { 58 | return &QueryNode{ 59 | Config: QueryConfig{ 60 | Server: "127.0.0.1:8086", 61 | Database: "db0", 62 | Command: "select * from cpu_load", 63 | }, 64 | } 65 | } 66 | 67 | // Type 实现 Node 接口,返回组件类型 68 | func (x *QueryNode) Type() string { 69 | return "x/opengeminiQuery" 70 | } 71 | 72 | // Init 初始化 OpenGemini 客户端 73 | func (x *QueryNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 74 | err := maps.Map2Struct(configuration, &x.Config) 75 | if err != nil { 76 | return err 77 | } 78 | x.WriteNode = &WriteNode{} 79 | if err = x.WriteNode.Init(ruleConfig, configuration); err != nil { 80 | return err 81 | } 82 | x.commandTemplate = str.NewTemplate(x.Config.Command) 83 | return nil 84 | } 85 | 86 | // OnMsg 实现 Node 接口,处理消息 87 | func (x *QueryNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 88 | database := x.Config.Database 89 | command := x.Config.Command 90 | if !x.databaseTemplate.IsNotVar() || !x.commandTemplate.IsNotVar() { 91 | evn := base.NodeUtils.GetEvnAndMetadata(ctx, msg) 92 | database = str.ExecuteTemplate(database, evn) 93 | command = str.ExecuteTemplate(command, evn) 94 | } 95 | 96 | q := opengemini.Query{ 97 | Database: database, 98 | Command: command, 99 | } 100 | if client, err := x.SharedNode.Get(); err != nil { 101 | ctx.TellFailure(msg, err) 102 | } else { 103 | if res, err := client.Query(q); err != nil { 104 | ctx.TellFailure(msg, err) 105 | } else { 106 | msg.DataType = types.JSON 107 | msg.SetData(str.ToString(res)) 108 | if err := hasError(res); err != nil { 109 | ctx.TellFailure(msg, err) 110 | } else { 111 | ctx.TellSuccess(msg) 112 | } 113 | } 114 | } 115 | 116 | } 117 | func hasError(result *opengemini.QueryResult) error { 118 | if len(result.Error) > 0 { 119 | return errors.New(result.Error) 120 | } 121 | for _, res := range result.Results { 122 | if len(res.Error) > 0 { 123 | return errors.New(res.Error) 124 | } 125 | } 126 | return nil 127 | } 128 | -------------------------------------------------------------------------------- /external/opengemini/query_test.go: -------------------------------------------------------------------------------- 1 | package opengemini 2 | 3 | import ( 4 | "github.com/openGemini/opengemini-client-go/opengemini" 5 | "github.com/rulego/rulego/api/types" 6 | "github.com/rulego/rulego/test" 7 | "github.com/rulego/rulego/test/assert" 8 | "strings" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestQueryNode(t *testing.T) { 14 | Registry := &types.SafeComponentSlice{} 15 | Registry.Add(&WriteNode{}) 16 | Registry.Add(&QueryNode{}) 17 | var queryNodeType = "x/opengeminiQuery" 18 | //var writeNodeType = "x/opengeminiWrite" 19 | 20 | t.Run("NewNode", func(t *testing.T) { 21 | test.NodeNew(t, queryNodeType, &QueryNode{}, types.Configuration{ 22 | "server": "127.0.0.1:8086", 23 | "database": "db0", 24 | "command": "select * from cpu_load", 25 | }, Registry) 26 | }) 27 | 28 | t.Run("InitNode", func(t *testing.T) { 29 | node, _ := test.CreateAndInitNode(queryNodeType, types.Configuration{ 30 | "server": "127.0.0.1:8086,127.0.0.1:8087", 31 | "database": "db0", 32 | "token": "aaa", 33 | }, Registry) 34 | assert.Equal(t, "aaa", node.(*QueryNode).opengeminiConfig.AuthConfig.Token) 35 | assert.Equal(t, "127.0.0.1", node.(*QueryNode).opengeminiConfig.Addresses[0].Host) 36 | assert.Equal(t, 8086, node.(*QueryNode).opengeminiConfig.Addresses[0].Port) 37 | assert.Equal(t, "127.0.0.1", node.(*QueryNode).opengeminiConfig.Addresses[1].Host) 38 | assert.Equal(t, 8087, node.(*QueryNode).opengeminiConfig.Addresses[1].Port) 39 | assert.Equal(t, opengemini.AuthTypeToken, node.(*QueryNode).opengeminiConfig.AuthConfig.AuthType) 40 | 41 | node, _ = test.CreateAndInitNode(queryNodeType, types.Configuration{ 42 | "server": "127.0.0.1:8086,192.168.0.1:8087", 43 | "database": "db0", 44 | "username": "aaa", 45 | "password": "bbb", 46 | "command": "select * from cpu_load", 47 | }, Registry) 48 | assert.Equal(t, "", node.(*QueryNode).opengeminiConfig.AuthConfig.Token) 49 | assert.Equal(t, "127.0.0.1", node.(*QueryNode).opengeminiConfig.Addresses[0].Host) 50 | assert.Equal(t, 8086, node.(*QueryNode).opengeminiConfig.Addresses[0].Port) 51 | assert.Equal(t, "192.168.0.1", node.(*QueryNode).opengeminiConfig.Addresses[1].Host) 52 | assert.Equal(t, 8087, node.(*QueryNode).opengeminiConfig.Addresses[1].Port) 53 | assert.Equal(t, opengemini.AuthTypePassword, node.(*QueryNode).opengeminiConfig.AuthConfig.AuthType) 54 | assert.Equal(t, "aaa", node.(*QueryNode).opengeminiConfig.AuthConfig.Username) 55 | assert.Equal(t, "bbb", node.(*QueryNode).opengeminiConfig.AuthConfig.Password) 56 | assert.Equal(t, "select * from cpu_load", node.(*QueryNode).Config.Command) 57 | }) 58 | t.Run("OnMsg", func(t *testing.T) { 59 | server := "8.134.32.225:8086" 60 | node1, err := test.CreateAndInitNode(queryNodeType, types.Configuration{ 61 | "server": server, 62 | "database": "db0", 63 | "command": "select * from cpu_load", 64 | }, Registry) 65 | assert.Nil(t, err) 66 | node2, _ := test.CreateAndInitNode(queryNodeType, types.Configuration{ 67 | "server": server, 68 | "database": "${metadata.database}", 69 | "command": "select * from ${msg.table}", 70 | }, Registry) 71 | node3, _ := test.CreateAndInitNode(queryNodeType, types.Configuration{ 72 | "server": server, 73 | "database": "db0", 74 | "command": "select * from xx", 75 | }, Registry) 76 | metaData := types.BuildMetadata(make(map[string]string)) 77 | metaData.PutValue("database", "db0") 78 | msgList := []test.Msg{ 79 | { 80 | MetaData: metaData, 81 | DataType: types.JSON, 82 | MsgType: "cpu_load_err", 83 | Data: "{\"table\":\"cpu_load\"}", 84 | }, 85 | } 86 | var nodeList = []test.NodeAndCallback{ 87 | { 88 | Node: node1, 89 | MsgList: msgList, 90 | Callback: func(msg types.RuleMsg, relationType string, err error) { 91 | assert.Equal(t, types.Success, relationType) 92 | }, 93 | }, { 94 | Node: node2, 95 | MsgList: msgList, 96 | Callback: func(msg types.RuleMsg, relationType string, err error) { 97 | assert.Equal(t, types.Success, relationType) 98 | }, 99 | }, { 100 | Node: node3, 101 | MsgList: msgList, 102 | Callback: func(msg types.RuleMsg, relationType string, err error) { 103 | assert.True(t, strings.Contains(msg.GetData(), "measurement not found")) 104 | assert.Equal(t, types.Failure, relationType) 105 | }, 106 | }, 107 | } 108 | for _, item := range nodeList { 109 | test.NodeOnMsgWithChildren(t, item.Node, item.MsgList, item.ChildrenNodes, item.Callback) 110 | } 111 | time.Sleep(time.Second * 5) 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /external/opengemini/utils.go: -------------------------------------------------------------------------------- 1 | package opengemini 2 | 3 | import ( 4 | "fmt" 5 | "github.com/openGemini/opengemini-client-go/opengemini" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // parseLineProtocol 解析单行 Line Protocol 字符串并返回 Point 结构体 12 | func parseLineProtocol(line string) (*opengemini.Point, error) { 13 | parts := strings.Split(line, " ") 14 | if len(parts) < 2 { 15 | return nil, fmt.Errorf("invalid line format") 16 | } 17 | 18 | var p opengemini.Point 19 | p.Tags = make(map[string]string) 20 | p.Fields = make(map[string]interface{}) 21 | p.Precision = opengemini.PrecisionNanosecond 22 | 23 | // 解析 measurement 和 tags 24 | measurementAndTags := strings.Split(parts[0], ",") 25 | //measurementEnd := strings.Index(measurementAndTags, " ") 26 | if len(parts) < 2 { 27 | return nil, fmt.Errorf("measurement name is missing") 28 | } 29 | p.Measurement = measurementAndTags[0] 30 | 31 | tags := measurementAndTags[1:] 32 | for _, tag := range tags { 33 | if tag == "" { 34 | continue 35 | } 36 | kv := strings.SplitN(tag, "=", 2) 37 | if len(kv) != 2 { 38 | return nil, fmt.Errorf("invalid tag format: %s", tag) 39 | } 40 | p.Tags[kv[0]] = kv[1] 41 | } 42 | 43 | // 解析 fields 44 | fieldsStr := parts[1] 45 | for _, field := range strings.Split(fieldsStr, ",") { 46 | if field == "" { 47 | continue 48 | } 49 | kv := strings.SplitN(field, "=", 2) 50 | if len(kv) != 2 { 51 | return nil, fmt.Errorf("invalid field format: %s", field) 52 | } 53 | value := kv[1] 54 | var fval interface{} 55 | parsedInt, err := strconv.ParseInt(value, 10, 64) 56 | if err == nil { 57 | fval = parsedInt 58 | } else { 59 | parsedFloat, err := strconv.ParseFloat(value, 64) 60 | if err == nil { 61 | fval = parsedFloat 62 | } else { 63 | // 如果既不是整数也不是浮点数,则保留原始字符串 64 | fval = value 65 | } 66 | } 67 | p.Fields[kv[0]] = fval 68 | } 69 | 70 | // 解析时间戳 71 | if len(parts) < 3 { 72 | p.Time = time.Now() 73 | } else { 74 | timestamp, err := strconv.ParseInt(parts[len(parts)-1], 10, 64) 75 | if err != nil { 76 | return nil, fmt.Errorf("invalid timestamp format: %s", parts[len(parts)-1]) 77 | } 78 | p.Time = time.Unix(0, timestamp) 79 | } 80 | 81 | return &p, nil 82 | } 83 | 84 | // parseMultiLineProtocol 接受一个包含多行 Line Protocol 数据的字符串,并返回解析后的 Point 列表 85 | func parseMultiLineProtocol(data string) ([]*opengemini.Point, error) { 86 | // 使用换行符分割字符串 87 | lines := strings.Split(data, "\n") 88 | var points []*opengemini.Point 89 | for _, line := range lines { 90 | line = strings.TrimSpace(line) // 移除行首尾的空白字符 91 | if line == "" { 92 | continue // 跳过空行 93 | } 94 | point, err := parseLineProtocol(line) 95 | if err != nil { 96 | return nil, fmt.Errorf("error parsing line: %s, error: %v", line, err) 97 | } 98 | points = append(points, point) 99 | } 100 | return points, nil 101 | } 102 | -------------------------------------------------------------------------------- /external/opengemini/utils_test.go: -------------------------------------------------------------------------------- 1 | package opengemini 2 | 3 | import ( 4 | "github.com/rulego/rulego/test/assert" 5 | "testing" 6 | ) 7 | 8 | func TestParseLineProtocol(t *testing.T) { 9 | line := "cpu_usage,host=server01,region=us-west value=23.5 1434055562000000000" 10 | 11 | point, err := parseLineProtocol(line) 12 | if err != nil { 13 | t.Fatal(t) 14 | } 15 | assert.Equal(t, "server01", point.Tags["host"]) 16 | assert.Equal(t, "us-west", point.Tags["region"]) 17 | assert.Equal(t, 23.5, point.Fields["value"]) 18 | } 19 | 20 | func TestParseMultiLineProtocol(t *testing.T) { 21 | lines := ` 22 | cpu_usage,host=server01,region=us-west value=23.5 1434055562000000000 23 | cpu_usage,host=server02,region=us-east value=45.6 1434055562000000000 24 | ` 25 | points, err := parseMultiLineProtocol(lines) 26 | if err != nil { 27 | t.Fatal(t) 28 | } 29 | assert.Equal(t, 2, len(points)) 30 | assert.Equal(t, "cpu_usage", points[0].Measurement) 31 | assert.Equal(t, "server01", points[0].Tags["host"]) 32 | assert.Equal(t, "us-west", points[0].Tags["region"]) 33 | assert.Equal(t, 23.5, points[0].Fields["value"]) 34 | 35 | assert.Equal(t, "cpu_usage", points[1].Measurement) 36 | assert.Equal(t, "server02", points[1].Tags["host"]) 37 | assert.Equal(t, "us-east", points[1].Tags["region"]) 38 | assert.Equal(t, 45.6, points[1].Fields["value"]) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /external/opengemini/write.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package opengemini 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "github.com/openGemini/opengemini-client-go/opengemini" 23 | "github.com/rulego/rulego" 24 | "github.com/rulego/rulego/api/types" 25 | "github.com/rulego/rulego/components/base" 26 | "github.com/rulego/rulego/utils/json" 27 | "github.com/rulego/rulego/utils/maps" 28 | "github.com/rulego/rulego/utils/str" 29 | "strconv" 30 | "strings" 31 | "time" 32 | ) 33 | 34 | func init() { 35 | _ = rulego.Registry.Register(&WriteNode{}) 36 | } 37 | 38 | // WriteConfig 定义 OpenGemini 客户端配置 39 | type WriteConfig struct { 40 | // Server 服务器地址,格式:host:port,多个地址用逗号分隔 41 | Server string 42 | // Database 数据库,允许使用 ${} 占位符变量 43 | Database string 44 | // Username 用户名 45 | Username string 46 | // Password 密码 47 | Password string 48 | // Token 如果 Token 不为空,使用 opengemini.AuthTypeToken 认证 49 | Token string 50 | } 51 | 52 | // WriteNode opengemini 写节点 53 | type WriteNode struct { 54 | base.SharedNode[opengemini.Client] 55 | client opengemini.Client 56 | Config WriteConfig 57 | opengeminiConfig *opengemini.Config 58 | databaseTemplate str.Template 59 | } 60 | 61 | // New 实现 Node 接口,创建新实例 62 | func (x *WriteNode) New() types.Node { 63 | return &WriteNode{ 64 | Config: WriteConfig{ 65 | Server: "127.0.0.1:8086", 66 | Database: "db0", 67 | }, 68 | } 69 | } 70 | 71 | // Type 实现 Node 接口,返回组件类型 72 | func (x *WriteNode) Type() string { 73 | return "x/opengeminiWrite" 74 | } 75 | 76 | // Init 初始化 OpenGemini 客户端 77 | func (x *WriteNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 78 | err := maps.Map2Struct(configuration, &x.Config) 79 | if err != nil { 80 | return err 81 | } 82 | if opengeminiConfig, err := x.createOpengeminiConfig(); err != nil { 83 | return err 84 | } else { 85 | x.opengeminiConfig = opengeminiConfig 86 | } 87 | x.databaseTemplate = str.NewTemplate(x.Config.Database) 88 | _ = x.SharedNode.Init(ruleConfig, x.Type(), x.Config.Server, ruleConfig.NodeClientInitNow, func() (opengemini.Client, error) { 89 | return x.initClient() 90 | }) 91 | return nil 92 | } 93 | 94 | // OnMsg 实现 Node 接口,处理消息 95 | func (x *WriteNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 96 | 97 | if client, err := x.SharedNode.Get(); err != nil { 98 | ctx.TellFailure(msg, err) 99 | } else { 100 | database := x.databaseTemplate.ExecuteFn(func() map[string]any { 101 | return base.NodeUtils.GetEvnAndMetadata(ctx, msg) 102 | }) 103 | var points []*opengemini.Point 104 | if msg.DataType == types.JSON { 105 | var point opengemini.Point 106 | //首先解析是否是多条 107 | if err := json.Unmarshal([]byte(msg.GetData()), &points); err != nil { 108 | //如果不是数组,则解析为单条 109 | if err := json.Unmarshal([]byte(msg.GetData()), &point); err != nil { 110 | ctx.TellFailure(msg, err) 111 | return 112 | } else { 113 | points = append(points, &point) 114 | } 115 | } 116 | } else { 117 | //解析 Line Protocol 118 | if points, err = parseMultiLineProtocol(msg.GetData()); err != nil { 119 | ctx.TellFailure(msg, err) 120 | return 121 | } 122 | } 123 | for _, point := range points { 124 | if point.Time.IsZero() { 125 | point.Time = time.Now() 126 | } 127 | } 128 | if err = client.WriteBatchPoints(context.Background(), database, points); err != nil { 129 | ctx.TellFailure(msg, err) 130 | } else { 131 | ctx.TellSuccess(msg) 132 | } 133 | } 134 | } 135 | 136 | // Destroy 清理资源 137 | func (x *WriteNode) Destroy() { 138 | if x.client != nil { 139 | _ = x.client.Close() 140 | } 141 | } 142 | 143 | func (x *WriteNode) GetInstance() (interface{}, error) { 144 | return x.SharedNode.GetInstance() 145 | } 146 | 147 | // initClient 初始化客户端 148 | func (x *WriteNode) initClient() (opengemini.Client, error) { 149 | if x.client != nil { 150 | return x.client, nil 151 | } else { 152 | x.Locker.Lock() 153 | defer x.Locker.Unlock() 154 | if x.client != nil { 155 | return x.client, nil 156 | } 157 | var err error 158 | // 创建 OpenGemini 客户端 159 | x.client, err = opengemini.NewClient(x.opengeminiConfig) 160 | return x.client, err 161 | } 162 | } 163 | 164 | func (x *WriteNode) createOpengeminiConfig() (*opengemini.Config, error) { 165 | var addresses []*opengemini.Address 166 | servers := strings.Split(x.Config.Server, ",") 167 | for _, server := range servers { 168 | addr := strings.Split(server, ":") 169 | if len(addr) < 2 { 170 | return nil, fmt.Errorf("must host:port format") 171 | } 172 | host := addr[0] 173 | if port, err := strconv.ParseInt(addr[1], 10, 64); err != nil { 174 | return nil, err 175 | } else { 176 | addresses = append(addresses, &opengemini.Address{ 177 | Host: host, 178 | Port: int(port), 179 | }) 180 | } 181 | } 182 | config := opengemini.Config{ 183 | Addresses: addresses, 184 | } 185 | var authConfig opengemini.AuthConfig 186 | if x.Config.Token != "" { 187 | authConfig.AuthType = opengemini.AuthTypeToken 188 | authConfig.Token = x.Config.Token 189 | config.AuthConfig = &authConfig 190 | } else if x.Config.Username != "" { 191 | authConfig.AuthType = opengemini.AuthTypePassword 192 | authConfig.Username = x.Config.Username 193 | authConfig.Password = x.Config.Password 194 | config.AuthConfig = &authConfig 195 | } 196 | 197 | return &config, nil 198 | } 199 | -------------------------------------------------------------------------------- /external/opengemini/write_test.go: -------------------------------------------------------------------------------- 1 | package opengemini 2 | 3 | import ( 4 | "github.com/openGemini/opengemini-client-go/opengemini" 5 | "github.com/rulego/rulego/api/types" 6 | "github.com/rulego/rulego/test" 7 | "github.com/rulego/rulego/test/assert" 8 | "github.com/rulego/rulego/utils/json" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestWriteNode(t *testing.T) { 14 | Registry := &types.SafeComponentSlice{} 15 | Registry.Add(&WriteNode{}) 16 | Registry.Add(&QueryNode{}) 17 | var writeNodeType = "x/opengeminiWrite" 18 | 19 | t.Run("NewNode", func(t *testing.T) { 20 | test.NodeNew(t, writeNodeType, &WriteNode{}, types.Configuration{ 21 | "server": "127.0.0.1:8086", 22 | "database": "db0", 23 | }, Registry) 24 | }) 25 | 26 | t.Run("InitNode", func(t *testing.T) { 27 | node, _ := test.CreateAndInitNode(writeNodeType, types.Configuration{ 28 | "server": "127.0.0.1:8086,127.0.0.1:8087", 29 | "database": "db0", 30 | "token": "aaa", 31 | }, Registry) 32 | assert.Equal(t, "aaa", node.(*WriteNode).opengeminiConfig.AuthConfig.Token) 33 | assert.Equal(t, "127.0.0.1", node.(*WriteNode).opengeminiConfig.Addresses[0].Host) 34 | assert.Equal(t, 8086, node.(*WriteNode).opengeminiConfig.Addresses[0].Port) 35 | assert.Equal(t, "127.0.0.1", node.(*WriteNode).opengeminiConfig.Addresses[1].Host) 36 | assert.Equal(t, 8087, node.(*WriteNode).opengeminiConfig.Addresses[1].Port) 37 | assert.Equal(t, opengemini.AuthTypeToken, node.(*WriteNode).opengeminiConfig.AuthConfig.AuthType) 38 | 39 | node, _ = test.CreateAndInitNode(writeNodeType, types.Configuration{ 40 | "server": "127.0.0.1:8086,192.168.0.1:8087", 41 | "database": "db0", 42 | "username": "aaa", 43 | "password": "bbb", 44 | }, Registry) 45 | assert.Equal(t, "", node.(*WriteNode).opengeminiConfig.AuthConfig.Token) 46 | assert.Equal(t, "127.0.0.1", node.(*WriteNode).opengeminiConfig.Addresses[0].Host) 47 | assert.Equal(t, 8086, node.(*WriteNode).opengeminiConfig.Addresses[0].Port) 48 | assert.Equal(t, "192.168.0.1", node.(*WriteNode).opengeminiConfig.Addresses[1].Host) 49 | assert.Equal(t, 8087, node.(*WriteNode).opengeminiConfig.Addresses[1].Port) 50 | assert.Equal(t, opengemini.AuthTypePassword, node.(*WriteNode).opengeminiConfig.AuthConfig.AuthType) 51 | assert.Equal(t, "aaa", node.(*WriteNode).opengeminiConfig.AuthConfig.Username) 52 | assert.Equal(t, "bbb", node.(*WriteNode).opengeminiConfig.AuthConfig.Password) 53 | }) 54 | t.Run("OnMsg", func(t *testing.T) { 55 | server := "8.134.32.225:8086" 56 | node, err := test.CreateAndInitNode(writeNodeType, types.Configuration{ 57 | "server": server, 58 | "database": "db0", 59 | }, Registry) 60 | assert.Nil(t, err) 61 | node2, err := test.CreateAndInitNode(writeNodeType, types.Configuration{ 62 | "server": server, 63 | "database": "${database}", 64 | }, Registry) 65 | 66 | node3, err := test.CreateAndInitNode(writeNodeType, types.Configuration{ 67 | "server": server, 68 | "database": "aa", 69 | }, Registry) 70 | 71 | insertPoint1 := opengemini.Point{ 72 | Measurement: "cpu_load", 73 | Tags: map[string]string{ 74 | "host": "server01", 75 | }, 76 | Fields: map[string]interface{}{ 77 | "value": 98.6, 78 | }, 79 | } 80 | insertPoint2 := opengemini.Point{ 81 | Measurement: "cpu_load", 82 | Tags: map[string]string{ 83 | "host": "server01", 84 | }, 85 | Fields: map[string]interface{}{ 86 | "value": 98.6, 87 | }, 88 | Time: time.Now(), 89 | } 90 | insertPoints := []opengemini.Point{insertPoint1, insertPoint2} 91 | insertData1, _ := json.Marshal(insertPoint1) 92 | insertData2, _ := json.Marshal(insertPoints) 93 | insertData3 := "[{\"measurement\":\"cpu_load\",\"Precision\":0,\"Time\":\"2024-09-03T13:41:27.3142051+08:00\",\"Tags\":{\"host\":\"server01\"},\"Fields\":{\"value\":98.6}}]" 94 | lineProtocol := "cpu_load,host=server01,region=us-west value=23.5 1434055562000000000" 95 | metaData := types.BuildMetadata(make(map[string]string)) 96 | metaData.PutValue("database", "db0") 97 | msgList := []test.Msg{ 98 | { 99 | MetaData: metaData, 100 | DataType: types.TEXT, 101 | MsgType: "cpu_load_err", 102 | Data: "AA", 103 | }, 104 | { 105 | MetaData: metaData, 106 | DataType: types.TEXT, 107 | MsgType: "cpu_load_line_protocol", 108 | Data: lineProtocol, 109 | }, 110 | { 111 | MetaData: metaData, 112 | DataType: types.JSON, 113 | MsgType: "cpu_load_json", 114 | Data: string(insertData1), 115 | }, 116 | { 117 | MetaData: metaData, 118 | DataType: types.JSON, 119 | MsgType: "cpu_load_json", 120 | Data: string(insertData2), 121 | }, 122 | { 123 | MetaData: metaData, 124 | DataType: types.JSON, 125 | MsgType: "cpu_load_json", 126 | Data: insertData3, 127 | }, 128 | } 129 | 130 | var nodeList = []test.NodeAndCallback{ 131 | { 132 | Node: node, 133 | MsgList: msgList, 134 | Callback: func(msg types.RuleMsg, relationType string, err error) { 135 | if msg.Type == "cpu_load_err" { 136 | assert.Equal(t, types.Failure, relationType) 137 | } else { 138 | assert.Equal(t, types.Success, relationType) 139 | } 140 | }, 141 | }, 142 | { 143 | Node: node2, 144 | MsgList: msgList, 145 | Callback: func(msg types.RuleMsg, relationType string, err error) { 146 | if msg.Type == "cpu_load_err" { 147 | assert.Equal(t, types.Failure, relationType) 148 | } else { 149 | assert.Equal(t, types.Success, relationType) 150 | } 151 | 152 | }, 153 | }, 154 | { 155 | Node: node3, 156 | MsgList: msgList, 157 | Callback: func(msg types.RuleMsg, relationType string, err error) { 158 | assert.Equal(t, types.Failure, relationType) 159 | }, 160 | }, 161 | } 162 | for _, item := range nodeList { 163 | test.NodeOnMsgWithChildren(t, item.Node, item.MsgList, item.ChildrenNodes, item.Callback) 164 | } 165 | time.Sleep(time.Second * 5) 166 | }) 167 | } 168 | -------------------------------------------------------------------------------- /external/rabbitmq/rabbitmq_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package rabbitmq 18 | 19 | import ( 20 | amqp "github.com/rabbitmq/amqp091-go" 21 | "github.com/rulego/rulego" 22 | "github.com/rulego/rulego/api/types" 23 | "github.com/rulego/rulego/components/base" 24 | "github.com/rulego/rulego/utils/maps" 25 | "github.com/rulego/rulego/utils/str" 26 | ) 27 | 28 | const ( 29 | ContentTypeJson = "application/json" 30 | ContentTypeText = "text/plain" 31 | 32 | KeyContentType = "Content-Type" 33 | KeyUTF8 = "utf-8" 34 | ) 35 | 36 | func init() { 37 | _ = rulego.Registry.Register(&ClientNode{}) 38 | } 39 | 40 | type ClientNodeConfiguration struct { 41 | // RabbitMQ服务器地址,格式为"amqp://用户名:密码@服务器地址:端口号" 42 | Server string 43 | // 路由键 44 | Key string 45 | // 交换机名称 46 | Exchange string 47 | // 交换机类型 direct, fanout, topic 48 | ExchangeType string 49 | //表示交换器是否持久化。如果设置为 true,即使消息服务器重启,交换器也会被保留。 50 | Durable bool 51 | //表示交换器是否自动删除。如果设置为 true,则当没有绑定的队列时,交换器会被自动删除。 52 | AutoDelete bool 53 | } 54 | 55 | type ClientNode struct { 56 | base.SharedNode[*amqp.Connection] 57 | // 节点配置 58 | Config ClientNodeConfiguration 59 | amqpConn *amqp.Connection 60 | amqpChannel *amqp.Channel 61 | // 是否正在连接RabbitMQ服务器 62 | connecting int32 63 | // 路由键模板 64 | keyTemplate str.Template 65 | } 66 | 67 | // Type 组件类型 68 | func (x *ClientNode) Type() string { 69 | return "x/rabbitmqClient" 70 | } 71 | 72 | func (x *ClientNode) New() types.Node { 73 | return &ClientNode{Config: ClientNodeConfiguration{ 74 | Server: "amqp://guest:guest@127.0.0.1:5672/", 75 | Exchange: "rulego", 76 | ExchangeType: "topic", 77 | Durable: true, 78 | AutoDelete: true, 79 | Key: "device.msg.request", 80 | }} 81 | } 82 | 83 | // Init 初始化 84 | func (x *ClientNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 85 | err := maps.Map2Struct(configuration, &x.Config) 86 | if err != nil { 87 | return err 88 | } 89 | _ = x.SharedNode.Init(ruleConfig, x.Type(), x.Config.Server, ruleConfig.NodeClientInitNow, func() (*amqp.Connection, error) { 90 | return x.initClient() 91 | }) 92 | x.keyTemplate = str.NewTemplate(x.Config.Key) 93 | return nil 94 | } 95 | 96 | // OnMsg 处理消息 97 | func (x *ClientNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 98 | var evn map[string]interface{} 99 | if !x.keyTemplate.IsNotVar() { 100 | evn = base.NodeUtils.GetEvnAndMetadata(ctx, msg) 101 | } 102 | key := x.keyTemplate.Execute(evn) 103 | 104 | ch, err := x.checkChannel() 105 | if err == nil { 106 | err = ch.Publish(x.Config.Exchange, key, false, false, 107 | amqp.Publishing{ 108 | ContentType: x.getContentType(msg), 109 | ContentEncoding: KeyUTF8, 110 | Body: []byte(msg.GetData()), 111 | }) 112 | } 113 | 114 | if err != nil { 115 | ctx.TellFailure(msg, err) 116 | } else { 117 | ctx.TellSuccess(msg) 118 | } 119 | } 120 | 121 | // Destroy 销毁 122 | func (x *ClientNode) Destroy() { 123 | if x.amqpChannel != nil { 124 | _ = x.amqpChannel.Close() 125 | } 126 | if x.amqpConn != nil { 127 | _ = x.amqpConn.Close() 128 | } 129 | } 130 | 131 | func (x *ClientNode) getContentType(msg types.RuleMsg) string { 132 | contentType := msg.Metadata.GetValue(KeyContentType) 133 | if contentType != "" { 134 | return contentType 135 | } else if msg.DataType == types.JSON { 136 | return ContentTypeJson 137 | } else { 138 | return ContentTypeText 139 | } 140 | } 141 | 142 | func (x *ClientNode) initClient() (*amqp.Connection, error) { 143 | if x.amqpConn != nil && !x.amqpConn.IsClosed() { 144 | return x.amqpConn, nil 145 | } else { 146 | x.Locker.Lock() 147 | defer x.Locker.Unlock() 148 | if x.amqpConn != nil && !x.amqpConn.IsClosed() { 149 | return x.amqpConn, nil 150 | } 151 | var err error 152 | x.amqpConn, err = amqp.Dial(x.Config.Server) 153 | return x.amqpConn, err 154 | } 155 | } 156 | 157 | func (x *ClientNode) checkChannel() (*amqp.Channel, error) { 158 | if x.amqpChannel != nil && !x.amqpChannel.IsClosed() { 159 | return x.amqpChannel, nil 160 | } 161 | var err error 162 | var conn *amqp.Connection 163 | conn, err = x.SharedNode.Get() 164 | if err != nil { 165 | return nil, err 166 | } 167 | x.Locker.Lock() 168 | defer x.Locker.Unlock() 169 | if x.amqpChannel != nil && !x.amqpChannel.IsClosed() { 170 | return x.amqpChannel, nil 171 | } 172 | x.amqpChannel, err = conn.Channel() 173 | if err != nil { 174 | return nil, err 175 | } 176 | if x.Config.Exchange != "" { 177 | //声明交换机 178 | err = x.amqpChannel.ExchangeDeclare( 179 | x.Config.Exchange, // 交换机名称 180 | x.Config.ExchangeType, // 交换机类型 181 | x.Config.Durable, //是否持久化 182 | x.Config.AutoDelete, //是否自动删除 183 | false, 184 | false, 185 | nil, 186 | ) 187 | if err != nil { 188 | //如果交换机已经存在,则不再声明,重新创建通道 189 | x.amqpChannel, err = conn.Channel() 190 | if err != nil { 191 | return nil, err 192 | } 193 | } 194 | } 195 | 196 | return x.amqpChannel, err 197 | } 198 | -------------------------------------------------------------------------------- /external/rabbitmq/rabbitmq_client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package rabbitmq 18 | 19 | import ( 20 | "github.com/rulego/rulego/api/types" 21 | "github.com/rulego/rulego/test" 22 | "github.com/rulego/rulego/test/assert" 23 | "testing" 24 | "time" 25 | ) 26 | 27 | func TestClientNode(t *testing.T) { 28 | Registry := &types.SafeComponentSlice{} 29 | Registry.Add(&ClientNode{}) 30 | var targetNodeType = "x/rabbitmqClient" 31 | 32 | t.Run("NewNode", func(t *testing.T) { 33 | test.NodeNew(t, targetNodeType, &ClientNode{}, types.Configuration{ 34 | "server": "amqp://guest:guest@127.0.0.1:5672/", 35 | "exchange": "rulego", 36 | "exchangeType": "topic", 37 | "durable": true, 38 | "autoDelete": true, 39 | "key": "device.msg.request", 40 | }, Registry) 41 | }) 42 | 43 | t.Run("InitNode", func(t *testing.T) { 44 | test.NodeInit(t, targetNodeType, types.Configuration{ 45 | "server": "amqp://guest:guest@127.0.0.1:5672/", 46 | "exchange": "rulego.topic.test", 47 | "exchangeType": "topic", 48 | "durable": true, 49 | "autoDelete": true, 50 | "key": "${metadata.key}", 51 | }, types.Configuration{ 52 | "server": "amqp://guest:guest@127.0.0.1:5672/", 53 | "exchange": "rulego.topic.test", 54 | "exchangeType": "topic", 55 | "durable": true, 56 | "autoDelete": true, 57 | "key": "${metadata.key}", 58 | }, Registry) 59 | }) 60 | 61 | t.Run("OnMsg", func(t *testing.T) { 62 | var server = "amqp://guest:guest@8.134.32.225:5672/" 63 | node1, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 64 | "server": server, 65 | "exchange": "rulego.topic.test", 66 | "exchangeType": "topic", 67 | "durable": true, 68 | "autoDelete": true, 69 | "forceDelete": true, 70 | "key": "${metadata.key}", 71 | }, Registry) 72 | assert.Nil(t, err) 73 | node2, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 74 | "server": server, 75 | "exchange": "rulego.topic.test", 76 | "exchangeType": "topic", 77 | "durable": false, 78 | "autoDelete": false, 79 | "key": "${metadata.key}", 80 | }, Registry) 81 | assert.Nil(t, err) 82 | node3, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 83 | "server": server, 84 | "exchange": "rulego.topic.test", 85 | "exchangeType": "topic", 86 | "durable": true, 87 | "autoDelete": false, 88 | "forceDelete": true, 89 | "key": "${metadata.key}", 90 | }, Registry) 91 | assert.Nil(t, err) 92 | metaData := types.BuildMetadata(make(map[string]string)) 93 | metaData.PutValue("key", "/device/msg") 94 | msgList := []test.Msg{ 95 | { 96 | MetaData: metaData, 97 | MsgType: "ACTIVITY_EVENT1", 98 | Data: "{\"temperature\":60}", 99 | AfterSleep: time.Millisecond * 200, 100 | }, 101 | { 102 | MetaData: metaData, 103 | MsgType: "ACTIVITY_EVENT2", 104 | Data: "{\"temperature\":61}", 105 | }, 106 | } 107 | var nodeList = []test.NodeAndCallback{ 108 | { 109 | Node: node1, 110 | MsgList: msgList, 111 | Callback: func(msg types.RuleMsg, relationType string, err error) { 112 | assert.Equal(t, types.Success, relationType) 113 | }, 114 | }, 115 | { 116 | Node: node2, 117 | MsgList: msgList, 118 | Callback: func(msg types.RuleMsg, relationType string, err error) { 119 | assert.Equal(t, types.Success, relationType) 120 | }, 121 | }, 122 | { 123 | Node: node3, 124 | MsgList: msgList, 125 | Callback: func(msg types.RuleMsg, relationType string, err error) { 126 | assert.Equal(t, types.Success, relationType) 127 | }, 128 | }, 129 | } 130 | for _, item := range nodeList { 131 | test.NodeOnMsgWithChildren(t, item.Node, item.MsgList, item.ChildrenNodes, item.Callback) 132 | } 133 | time.Sleep(time.Second * 10) 134 | }) 135 | } 136 | -------------------------------------------------------------------------------- /external/redis/redis_client.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package redis 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "github.com/expr-lang/expr" 23 | "github.com/expr-lang/expr/vm" 24 | "github.com/redis/go-redis/v9" 25 | "github.com/rulego/rulego" 26 | "github.com/rulego/rulego/api/types" 27 | "github.com/rulego/rulego/components/base" 28 | "github.com/rulego/rulego/utils/maps" 29 | "github.com/rulego/rulego/utils/str" 30 | "strings" 31 | ) 32 | 33 | // 注册节点 34 | func init() { 35 | _ = rulego.Registry.Register(&ClientNode{}) 36 | } 37 | 38 | // ClientNodeConfiguration 节点配置 39 | type ClientNodeConfiguration struct { 40 | // Server redis服务器地址 41 | Server string 42 | // Password 密码 43 | Password string 44 | // PoolSize 连接池大小 45 | PoolSize int 46 | // Db 数据库index 47 | Db int 48 | // Cmd 执行命令,例如SET/GET/DEL 49 | // 支持${metadata.key}占位符读取metadata元数据 50 | // 支持${msg.key}占位符读取消息负荷指定key数据 51 | // 支持${data}获取消息原始负荷 52 | Cmd string 53 | // ParamsExpr 动态参数表达式。ParamsExpr和Params同时存在则优先使用ParamsExpr 54 | ParamsExpr string 55 | // Params 执行命令参数 56 | // 支持${metadata.key}占位符读取metadata元数据 57 | // 支持${msg.key}占位符读取消息负荷指定key数据 58 | // 支持${data}获取消息原始负荷 59 | Params []interface{} 60 | } 61 | 62 | // ClientNode redis客户端节点, 63 | // 成功:转向Success链,redis执行结果存放在msg.Data 64 | // 失败:转向Failure链 65 | type ClientNode struct { 66 | base.SharedNode[*redis.Client] 67 | //节点配置 68 | Config ClientNodeConfiguration 69 | client *redis.Client 70 | //cmd是否有变量 71 | cmdHasVar bool 72 | //参数是否有变量 73 | paramsHasVar bool 74 | paramsExprProgram *vm.Program 75 | } 76 | 77 | // Type 返回组件类型 78 | func (x *ClientNode) Type() string { 79 | return "x/redisClient" 80 | } 81 | 82 | func (x *ClientNode) New() types.Node { 83 | return &ClientNode{Config: ClientNodeConfiguration{ 84 | Server: "127.0.0.1:6379", 85 | Cmd: "GET", 86 | Params: []interface{}{"${metadata.key}"}, 87 | Db: 0, 88 | }} 89 | } 90 | 91 | // Init 初始化组件 92 | func (x *ClientNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 93 | err := maps.Map2Struct(configuration, &x.Config) 94 | if err == nil { 95 | //初始化客户端 96 | _ = x.SharedNode.Init(ruleConfig, x.Type(), x.Config.Server, ruleConfig.NodeClientInitNow, func() (*redis.Client, error) { 97 | return x.initClient() 98 | }) 99 | 100 | if str.CheckHasVar(x.Config.Cmd) { 101 | x.cmdHasVar = true 102 | } 103 | //检查是参数否有变量 104 | for _, item := range x.Config.Params { 105 | if v, ok := item.(string); ok && str.CheckHasVar(v) { 106 | x.paramsHasVar = true 107 | break 108 | } 109 | } 110 | 111 | if exprV := strings.TrimSpace(x.Config.ParamsExpr); exprV != "" { 112 | if program, err := expr.Compile(exprV, expr.AllowUndefinedVariables()); err != nil { 113 | return err 114 | } else { 115 | x.paramsExprProgram = program 116 | } 117 | } 118 | } 119 | return err 120 | } 121 | 122 | // OnMsg 处理消息 123 | func (x *ClientNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 124 | var data interface{} 125 | var err error 126 | var evn map[string]interface{} 127 | if x.cmdHasVar || x.paramsHasVar || x.paramsExprProgram != nil { 128 | evn = base.NodeUtils.GetEvnAndMetadata(ctx, msg) 129 | } 130 | var cmd = x.Config.Cmd 131 | if x.cmdHasVar { 132 | cmd = str.ExecuteTemplate(x.Config.Cmd, evn) 133 | } 134 | cmd = strings.ToLower(strings.TrimSpace(cmd)) 135 | 136 | var args []interface{} 137 | args = append(args, cmd) 138 | if x.paramsExprProgram != nil { 139 | var exprVm = vm.VM{} 140 | if out, err := exprVm.Run(x.paramsExprProgram, evn); err != nil { 141 | ctx.TellFailure(msg, err) 142 | return 143 | } else { 144 | if v, ok := out.([]interface{}); ok { 145 | args = append(args, v...) 146 | } else { 147 | args = append(args, out) 148 | } 149 | } 150 | } else if x.paramsHasVar { 151 | for _, item := range x.Config.Params { 152 | if itemStr, ok := item.(string); ok { 153 | args = append(args, str.ExecuteTemplate(itemStr, evn)) 154 | } else { 155 | args = append(args, item) 156 | } 157 | } 158 | } else { 159 | args = append(args, x.Config.Params...) 160 | } 161 | 162 | client, err := x.SharedNode.Get() 163 | if err != nil { 164 | ctx.TellFailure(msg, err) 165 | return 166 | } 167 | if cmd == "hgetall" { 168 | if len(args) < 2 { 169 | ctx.TellFailure(msg, fmt.Errorf("hgetall need one param")) 170 | return 171 | } 172 | //hgetall特殊处理强制,返回值转换成确定的map[string][string]类型 173 | data, err = client.HGetAll(ctx.GetContext(), str.ToString(args[1])).Result() 174 | } else { 175 | //请求redis服务器,并得到返回结果 176 | data, err = client.Do(ctx.GetContext(), args...).Result() 177 | } 178 | 179 | if err != nil { 180 | ctx.TellFailure(msg, err) 181 | } else { 182 | msg.SetData(str.ToString(data)) 183 | ctx.TellSuccess(msg) 184 | } 185 | } 186 | 187 | // Destroy 销毁组件 188 | func (x *ClientNode) Destroy() { 189 | if x.client != nil { 190 | _ = x.client.Close() 191 | } 192 | } 193 | 194 | func (x *ClientNode) initClient() (*redis.Client, error) { 195 | if x.client != nil { 196 | return x.client, nil 197 | } else { 198 | x.Locker.Lock() 199 | defer x.Locker.Unlock() 200 | if x.client != nil { 201 | return x.client, nil 202 | } 203 | x.client = redis.NewClient(&redis.Options{ 204 | Addr: x.Config.Server, 205 | PoolSize: x.Config.PoolSize, 206 | DB: x.Config.Db, 207 | Password: x.Config.Password, 208 | }) 209 | return x.client, x.client.Ping(context.Background()).Err() 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /external/redis/redis_publisher.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/redis/go-redis/v9" 8 | "github.com/rulego/rulego" 9 | "github.com/rulego/rulego/api/types" 10 | "github.com/rulego/rulego/components/base" 11 | "github.com/rulego/rulego/utils/maps" 12 | "github.com/rulego/rulego/utils/str" 13 | ) 14 | 15 | // 注册节点 16 | func init() { 17 | _ = rulego.Registry.Register(&PublisherNode{}) 18 | } 19 | 20 | // KeyResult 接收到消息的订阅者数量 21 | const KeyResult = "result" 22 | 23 | // PublisherNodeConfiguration 节点配置 24 | type PublisherNodeConfiguration struct { 25 | // Server redis服务器地址 26 | Server string 27 | // Password 密码 28 | Password string 29 | // PoolSize 连接池大小 30 | PoolSize int 31 | // Db 数据库index 32 | Db int 33 | // Channel 发布频道 34 | // 支持${metadata.key}占位符读取metadata元数据 35 | Channel string 36 | } 37 | 38 | // PublisherNode redis发布节点 39 | // 成功:转向Success链,通过msg.metadata.result获取接收到消息的订阅者数量 40 | // 失败:转向Failure链 41 | type PublisherNode struct { 42 | base.SharedNode[*redis.Client] 43 | //节点配置 44 | Config PublisherNodeConfiguration 45 | client *redis.Client 46 | channelTemplate str.Template 47 | } 48 | 49 | // Type 返回组件类型 50 | func (x *PublisherNode) Type() string { 51 | return "x/redisPub" 52 | } 53 | 54 | func (x *PublisherNode) New() types.Node { 55 | return &PublisherNode{Config: PublisherNodeConfiguration{ 56 | Server: "127.0.0.1:6379", 57 | Channel: "default", 58 | Db: 0, 59 | }} 60 | } 61 | 62 | // Init 初始化组件 63 | func (x *PublisherNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 64 | err := maps.Map2Struct(configuration, &x.Config) 65 | if err == nil { 66 | //初始化客户端 67 | _ = x.SharedNode.Init(ruleConfig, x.Type(), x.Config.Server, ruleConfig.NodeClientInitNow, func() (*redis.Client, error) { 68 | return x.initClient() 69 | }) 70 | x.channelTemplate = str.NewTemplate(strings.TrimSpace(x.Config.Channel)) 71 | } 72 | return err 73 | } 74 | 75 | // OnMsg 处理消息 76 | func (x *PublisherNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 77 | evn := base.NodeUtils.GetEvnAndMetadata(ctx, msg) 78 | var channel = x.channelTemplate.Execute(evn) 79 | client, err := x.SharedNode.Get() 80 | if err != nil { 81 | ctx.TellFailure(msg, err) 82 | return 83 | } 84 | 85 | // 发布消息到Redis 86 | result, err := client.Publish(ctx.GetContext(), channel, msg.Data).Result() 87 | if err != nil { 88 | ctx.TellFailure(msg, err) 89 | } else { 90 | msg.Metadata.PutValue(KeyResult, str.ToString(result)) 91 | ctx.TellSuccess(msg) 92 | } 93 | } 94 | 95 | // Destroy 销毁组件 96 | func (x *PublisherNode) Destroy() { 97 | if x.client != nil { 98 | _ = x.client.Close() 99 | } 100 | } 101 | 102 | func (x *PublisherNode) initClient() (*redis.Client, error) { 103 | if x.client != nil { 104 | return x.client, nil 105 | } else { 106 | x.Locker.Lock() 107 | defer x.Locker.Unlock() 108 | if x.client != nil { 109 | return x.client, nil 110 | } 111 | x.client = redis.NewClient(&redis.Options{ 112 | Addr: x.Config.Server, 113 | PoolSize: x.Config.PoolSize, 114 | DB: x.Config.Db, 115 | Password: x.Config.Password, 116 | }) 117 | return x.client, x.client.Ping(context.Background()).Err() 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /external/redis/redis_publisher_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/rulego/rulego/api/types" 8 | "github.com/rulego/rulego/test" 9 | "github.com/rulego/rulego/test/assert" 10 | ) 11 | 12 | func TestPublisherNode(t *testing.T) { 13 | Registry := &types.SafeComponentSlice{} 14 | Registry.Add(&PublisherNode{}) 15 | var targetNodeType = "x/redisPub" 16 | 17 | t.Run("NewNode", func(t *testing.T) { 18 | test.NodeNew(t, targetNodeType, &PublisherNode{}, types.Configuration{ 19 | "server": "127.0.0.1:6379", 20 | "password": "", 21 | "poolSize": 0, 22 | "db": 0, 23 | "channel": "default", 24 | }, Registry) 25 | }) 26 | 27 | t.Run("InitNode", func(t *testing.T) { 28 | test.NodeInit(t, targetNodeType, types.Configuration{ 29 | "server": "127.0.0.1:6379", 30 | "channel": "${metadata.channel}", 31 | "password": "", 32 | "poolSize": 10, 33 | "db": 0, 34 | }, types.Configuration{ 35 | "server": "127.0.0.1:6379", 36 | "channel": "${metadata.channel}", 37 | "password": "", 38 | "poolSize": 10, 39 | "db": 0, 40 | }, Registry) 41 | }) 42 | 43 | t.Run("OnMsg", func(t *testing.T) { 44 | var server = "127.0.0.1:6379" 45 | node1, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 46 | "server": server, 47 | "channel": "${metadata.channel}", 48 | }, Registry) 49 | assert.Nil(t, err) 50 | 51 | node2, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 52 | "server": server, 53 | "channel": "test-channel", 54 | }, Registry) 55 | assert.Nil(t, err) 56 | 57 | metaData := types.BuildMetadata(make(map[string]string)) 58 | metaData.PutValue("channel", "device/msg") 59 | msgList := []test.Msg{ 60 | { 61 | MetaData: metaData, 62 | Data: "test message", 63 | AfterSleep: time.Millisecond * 100, 64 | }, 65 | } 66 | var nodeList = []test.NodeAndCallback{ 67 | { 68 | Node: node1, 69 | MsgList: msgList, 70 | Callback: func(msg types.RuleMsg, relationType string, err error) { 71 | assert.Equal(t, types.Success, relationType) 72 | }, 73 | }, 74 | { 75 | Node: node2, 76 | MsgList: msgList, 77 | Callback: func(msg types.RuleMsg, relationType string, err error) { 78 | assert.Equal(t, types.Success, relationType) 79 | }, 80 | }, 81 | } 82 | for _, item := range nodeList { 83 | test.NodeOnMsgWithChildren(t, item.Node, item.MsgList, item.ChildrenNodes, item.Callback) 84 | } 85 | time.Sleep(time.Millisecond * 100) 86 | }) 87 | } 88 | -------------------------------------------------------------------------------- /external/wukongim/wukongim_sender.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package wukongim 18 | 19 | import ( 20 | "strconv" 21 | "time" 22 | 23 | wkproto "github.com/WuKongIM/WuKongIMGoProto" 24 | "github.com/WuKongIM/WuKongIMGoSDK/pkg/wksdk" 25 | "github.com/rulego/rulego" 26 | "github.com/rulego/rulego/api/types" 27 | "github.com/rulego/rulego/components/base" 28 | "github.com/rulego/rulego/utils/maps" 29 | "github.com/rulego/rulego/utils/str" 30 | ) 31 | 32 | // 注册节点 33 | func init() { 34 | _ = rulego.Registry.Register(&WukongimSender{}) 35 | } 36 | 37 | // WukongimSenderConfiguration 节点配置 38 | type WukongimSenderConfiguration struct { 39 | // 服务器地址 40 | Server string 41 | // 用户UID 42 | UID string 43 | // 登录密码 44 | Token string 45 | // 频道类型 允许使用 ${} 占位符变量 46 | // ChannelTypePerson uint8 = 1 // 个人频道 47 | // ChannelTypeGroup uint8 = 2 // 群组频道 48 | // ChannelTypeCustomerService uint8 = 3 // 客服频道 49 | // ChannelTypeCommunity uint8 = 4 // 社区频道 50 | // ChannelTypeCommunityTopic uint8 = 5 // 社区话题频道 51 | // ChannelTypeInfo uint8 = 6 // 资讯频道(有临时订阅者的概念,查看资讯的时候加入临时订阅,退出资讯的时候退出临时订阅) 52 | // ChannelTypeData uint8 = 7 // 数据频道 53 | ChannelType string 54 | // 频道ID 允许使用 ${} 占位符变量 55 | // 如果 ChannelType=1 则填写用户:UID 56 | ChannelID string 57 | // 连接超时,单位秒 58 | ConnectTimeout int64 59 | // Proto版本 60 | ProtoVersion int 61 | // 心跳间隔,单位秒 62 | PingInterval int64 63 | // 是否自动重连 64 | Reconnect bool 65 | // 是否自动确认消息 66 | AutoAck bool 67 | // 是否不存储,默认 false 68 | NoPersist bool 69 | // 是否同步一次(写模式),默认 false 70 | SyncOnce bool 71 | // 是否显示红点,默认true 72 | RedDot bool 73 | // 是否不需要加密,默认false 74 | NoEncrypt bool 75 | } 76 | 77 | // WukongimSender wksdk.Client客户端节点, 78 | // 成功:转向Success链,发送消息执行结果存放在msg.Data 79 | // 失败:转向Failure链 80 | type WukongimSender struct { 81 | base.SharedNode[*wksdk.Client] 82 | //节点配置 83 | Config WukongimSenderConfiguration 84 | client *wksdk.Client 85 | channelIdTemplate str.Template 86 | channelTypeTemplate str.Template 87 | } 88 | 89 | // Type 返回组件类型 90 | func (x *WukongimSender) Type() string { 91 | return "x/wukongimSender" 92 | } 93 | 94 | func (x *WukongimSender) New() types.Node { 95 | return &WukongimSender{Config: WukongimSenderConfiguration{ 96 | Server: "tcp://175.27.245.108:15100", 97 | UID: "test1", 98 | Token: "test1", 99 | ConnectTimeout: 5, 100 | ProtoVersion: wkproto.LatestVersion, 101 | PingInterval: 30, 102 | Reconnect: true, 103 | AutoAck: true, 104 | ChannelType: "1", 105 | ChannelID: "test2", 106 | NoPersist: false, 107 | SyncOnce: false, 108 | RedDot: true, 109 | NoEncrypt: false, 110 | }} 111 | } 112 | 113 | // Init 初始化组件 114 | func (x *WukongimSender) Init(ruleConfig types.Config, configuration types.Configuration) error { 115 | err := maps.Map2Struct(configuration, &x.Config) 116 | if err == nil { 117 | //初始化客户端 118 | err = x.SharedNode.Init(ruleConfig, x.Type(), x.Config.Server, ruleConfig.NodeClientInitNow, func() (*wksdk.Client, error) { 119 | return x.initClient() 120 | }) 121 | } 122 | x.channelIdTemplate = str.NewTemplate(x.Config.ChannelID) 123 | x.channelTypeTemplate = str.NewTemplate(x.Config.ChannelType) 124 | return err 125 | } 126 | 127 | // OnMsg 处理消息 128 | func (x *WukongimSender) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 129 | ctype := x.Config.ChannelType 130 | cid := x.Config.ChannelID 131 | var utype uint64 = 1 132 | var err error 133 | if !x.channelIdTemplate.IsNotVar() || !x.channelTypeTemplate.IsNotVar() { 134 | evn := base.NodeUtils.GetEvnAndMetadata(ctx, msg) 135 | ctype = x.channelTypeTemplate.Execute(evn) 136 | cid = x.channelIdTemplate.Execute(evn) 137 | } 138 | client, err := x.SharedNode.Get() 139 | if err != nil { 140 | ctx.TellFailure(msg, err) 141 | return 142 | } 143 | utype, err = strconv.ParseUint(ctype, 10, 8) 144 | if err != nil { 145 | ctx.TellFailure(msg, err) 146 | return 147 | } 148 | packet, err := client.SendMessage([]byte(msg.GetData()), 149 | wkproto.Channel{ 150 | ChannelType: uint8(utype), 151 | ChannelID: cid, 152 | }, 153 | wksdk.SendOptionWithNoPersist(x.Config.NoPersist), 154 | wksdk.SendOptionWithSyncOnce(x.Config.SyncOnce), 155 | wksdk.SendOptionWithRedDot(x.Config.RedDot), 156 | wksdk.SendOptionWithNoEncrypt(x.Config.NoEncrypt)) 157 | if err != nil { 158 | ctx.TellFailure(msg, err) 159 | } else { 160 | msg.SetData(str.ToString(packet)) 161 | ctx.TellSuccess(msg) 162 | } 163 | } 164 | 165 | // Destroy 销毁组件 166 | func (x *WukongimSender) Destroy() { 167 | if x.client != nil { 168 | _ = x.client.Disconnect() 169 | } 170 | } 171 | 172 | func (x *WukongimSender) initClient() (*wksdk.Client, error) { 173 | if x.client != nil { 174 | return x.client, nil 175 | } else { 176 | x.Locker.Lock() 177 | defer x.Locker.Unlock() 178 | if x.client != nil { 179 | return x.client, nil 180 | } 181 | x.client = wksdk.NewClient(x.Config.Server, 182 | wksdk.WithConnectTimeout(time.Duration(x.Config.ConnectTimeout)*time.Second), 183 | wksdk.WithProtoVersion(x.Config.ProtoVersion), 184 | wksdk.WithUID(x.Config.UID), 185 | wksdk.WithToken(x.Config.Token), 186 | wksdk.WithPingInterval(time.Duration(x.Config.PingInterval)*time.Second), 187 | wksdk.WithReconnect(x.Config.Reconnect), 188 | wksdk.WithAutoAck(x.Config.AutoAck), 189 | ) 190 | err := x.client.Connect() 191 | return x.client, err 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /filter/README.md: -------------------------------------------------------------------------------- 1 | # filter 2 | Filter messages. 3 | 4 | ## How to customize components 5 | Implement the `types.Node` interface. 6 | Example of a custom component: 7 | ```go 8 | // Define the Node component 9 | // UpperNode A plugin that converts the message data to uppercase 10 | type UpperNode struct{} 11 | 12 | func (n *UpperNode) Type() string { 13 | return "test/upper" 14 | } 15 | 16 | func (n *UpperNode) New() types.Node { 17 | return &UpperNode{} 18 | } 19 | 20 | func (n *UpperNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 21 | // Do some initialization work 22 | return nil 23 | } 24 | 25 | // Process the message 26 | func (n *UpperNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 27 | msg.Data = strings.ToUpper(msg.Data) 28 | // Send the modified message to the next node 29 | ctx.TellSuccess(msg) 30 | return nil 31 | } 32 | 33 | func (n *UpperNode) Destroy() { 34 | // Do some cleanup work 35 | } 36 | ``` 37 | 38 | ## Usage 39 | 40 | Register the component to the `RuleGo` default registry: 41 | ```go 42 | rulego.Registry.Register(&MyNode{}) 43 | ``` 44 | 45 | Then use your component in the rule chain DSL file: 46 | ```json 47 | { 48 | "ruleChain": { 49 | "name": "Test rule chain", 50 | "root": true, 51 | "debugMode": false 52 | }, 53 | "metadata": { 54 | "nodes": [ 55 | { 56 | "id": "s1", 57 | "type": "test/upper", 58 | "name": "Name", 59 | "debugMode": true, 60 | "configuration": { 61 | "field1": "Configuration parameters defined by the component", 62 | "....": "..." 63 | } 64 | } 65 | ], 66 | "connections": [ 67 | { 68 | "fromId": "s1", 69 | "toId": "Connect to the next component ID", 70 | "type": "The connection relationship with the component" 71 | } 72 | ] 73 | } 74 | } 75 | ``` -------------------------------------------------------------------------------- /filter/README_ZH.md: -------------------------------------------------------------------------------- 1 | # filter 2 | 对消息进行过滤。 3 | 4 | ## 怎样自定义组件 5 | 实现`types.Node`接口,例子: 6 | ```go 7 | //定义Node组件 8 | //UpperNode A plugin that converts the message data to uppercase 9 | type UpperNode struct{} 10 | 11 | func (n *UpperNode) Type() string { 12 | return "test/upper" 13 | } 14 | func (n *UpperNode) New() types.Node { 15 | return &UpperNode{} 16 | } 17 | func (n *UpperNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 18 | // Do some initialization work 19 | return nil 20 | } 21 | //处理消息 22 | func (n *UpperNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 23 | msg.Data = strings.ToUpper(msg.Data) 24 | // Send the modified message to the next node 25 | ctx.TellSuccess(msg) 26 | return nil 27 | } 28 | 29 | func (n *UpperNode) Destroy() { 30 | // Do some cleanup work 31 | } 32 | ``` 33 | 34 | ## 使用 35 | 36 | 把组件注册到`RuleGo`默认注册器 37 | ```go 38 | rulego.Registry.Register(&MyNode{}) 39 | ``` 40 | 41 | 然后在规则链DSL文件使用您的组件 42 | ```json 43 | { 44 | "ruleChain": { 45 | "name": "测试规则链", 46 | "root": true, 47 | "debugMode": false 48 | }, 49 | "metadata": { 50 | "nodes": [ 51 | { 52 | "id": "s1", 53 | "type": "test/upper", 54 | "name": "名称", 55 | "debugMode": true, 56 | "configuration": { 57 | "field1": "组件定义的配置参数", 58 | "....": "..." 59 | } 60 | } 61 | ], 62 | "connections": [ 63 | { 64 | "fromId": "s1", 65 | "toId": "连接下一个组件ID", 66 | "type": "与组件的连接关系" 67 | } 68 | ] 69 | } 70 | } 71 | ``` -------------------------------------------------------------------------------- /filter/lua_filter.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package filter 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "github.com/rulego/rulego" 23 | "github.com/rulego/rulego-components/pkg/lua_engine" 24 | "github.com/rulego/rulego/api/types" 25 | "github.com/rulego/rulego/utils/maps" 26 | lua "github.com/yuin/gopher-lua" 27 | "strings" 28 | ) 29 | 30 | // init registers the component to rulego 31 | func init() { 32 | _ = rulego.Registry.Register(&LuaFilter{}) 33 | } 34 | 35 | // LuaFilterConfiguration node configuration 36 | type LuaFilterConfiguration struct { 37 | //Script configures the function body content or the script file path with `.lua` as the suffix 38 | //Only need to provide the function body content, if it is a file path, then need to provide the complete script function: 39 | //function Filter(msg, metadata, msgType) ${Script} \n end 40 | //return bool 41 | //The parameter msg, if the data type of msg is JSON, then it will be converted to the Lua table type before calling the function 42 | Script string 43 | } 44 | 45 | // LuaFilter is a component that filters messages based on Lua scripts. 46 | type LuaFilter struct { 47 | Config LuaFilterConfiguration 48 | // pool is a sync.Pool of *lua.LState 49 | pool *luaEngine.LStatePool 50 | } 51 | 52 | // New creates a new instance of LuaFilter 53 | func (x *LuaFilter) New() types.Node { 54 | return &LuaFilter{Config: LuaFilterConfiguration{ 55 | Script: "return msg.temperature > 50", 56 | }} 57 | } 58 | 59 | // Type returns the type of the component 60 | func (x *LuaFilter) Type() string { 61 | return "x/luaFilter" 62 | } 63 | 64 | // Init initializes the component 65 | func (x *LuaFilter) Init(ruleConfig types.Config, configuration types.Configuration) error { 66 | err := maps.Map2Struct(configuration, &x.Config) 67 | if err == nil { 68 | 69 | if strings.HasSuffix(x.Config.Script, ".lua") { 70 | if err = luaEngine.ValidateLua(x.Config.Script); err != nil { 71 | return err 72 | } 73 | // create a new LStatePool from file 74 | x.pool = luaEngine.NewFileLStatePool(ruleConfig, x.Config.Script, configuration) 75 | } else { 76 | script := fmt.Sprintf("function Filter(msg, metadata, msgType) %s \nend", x.Config.Script) 77 | if err = luaEngine.ValidateLua(script); err != nil { 78 | return err 79 | } 80 | // create a new LStatePool from script 81 | x.pool = luaEngine.NewStringLStatePool(ruleConfig, script, configuration) 82 | } 83 | 84 | } 85 | return err 86 | } 87 | 88 | // OnMsg handles the message 89 | func (x *LuaFilter) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 90 | // get a *lua.LState from the pool 91 | L := x.pool.Get() 92 | if L == nil { 93 | // if there is no available *lua.LState, tell the next node to fail 94 | ctx.TellFailure(msg, fmt.Errorf("x/luaFilter lua.LState nil error")) 95 | return 96 | } 97 | // defer putting back the *lua.LState to the pool 98 | defer x.pool.Put(L) 99 | 100 | //var data interface{} = msg.Data 101 | var dataMap map[string]interface{} 102 | if msg.DataType == types.JSON { 103 | _ = json.Unmarshal([]byte(msg.GetData()), &dataMap) 104 | } 105 | var err error 106 | filter := L.GetGlobal("Filter") 107 | p := lua.P{ 108 | Fn: filter, 109 | NRet: 1, 110 | Protect: true, 111 | } 112 | if dataMap != nil { 113 | // Call the Filter function, passing in msg, metadata, msgType as arguments. 114 | err = L.CallByParam(p, luaEngine.MapToLTable(L, dataMap), luaEngine.StringMapToLTable(L, msg.Metadata.Values()), lua.LString(msg.Type)) 115 | } else { 116 | // Call the Filter function, passing in msg, metadata, msgType as arguments. 117 | err = L.CallByParam(p, lua.LString(msg.GetData()), luaEngine.StringMapToLTable(L, msg.Metadata.Values()), lua.LString(msg.Type)) 118 | } 119 | 120 | if err != nil { 121 | // if there is an error, tell the next node to fail 122 | ctx.TellFailure(msg, err) 123 | return 124 | } 125 | // get the return value from the script 126 | ret := L.Get(-1) 127 | // pop the value from the stack 128 | L.Pop(1) 129 | // check if the return value is a boolean 130 | if ret.Type() == lua.LTBool && ret == lua.LTrue { 131 | ctx.TellNext(msg, types.True) 132 | } else { 133 | ctx.TellNext(msg, types.False) 134 | } 135 | } 136 | 137 | // Destroy releases the resources of the component 138 | func (x *LuaFilter) Destroy() { 139 | if x.pool != nil { 140 | x.pool.Shutdown() 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /filter/lua_filter_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package filter 18 | 19 | import ( 20 | "github.com/rulego/rulego/api/types" 21 | "github.com/rulego/rulego/test" 22 | "github.com/rulego/rulego/test/assert" 23 | "testing" 24 | "time" 25 | ) 26 | 27 | func TestLuaFilter(t *testing.T) { 28 | var targetNodeType = "x/luaFilter" 29 | var registry = &types.SafeComponentSlice{} 30 | registry.Add(&LuaFilter{}) 31 | 32 | t.Run("NewNode", func(t *testing.T) { 33 | test.NodeNew(t, targetNodeType, &LuaFilter{}, types.Configuration{}, registry) 34 | }) 35 | 36 | t.Run("InitNode", func(t *testing.T) { 37 | test.NodeInit(t, targetNodeType, types.Configuration{ 38 | "script": "return msg.temperature > 60", 39 | }, types.Configuration{ 40 | "script": "return msg.temperature > 60", 41 | }, registry) 42 | }) 43 | 44 | t.Run("DefaultConfig", func(t *testing.T) { 45 | test.NodeInit(t, targetNodeType, types.Configuration{ 46 | "script": "return msg.temperature > 50", 47 | }, types.Configuration{ 48 | "script": "return msg.temperature > 50", 49 | }, registry) 50 | }) 51 | 52 | t.Run("OnMsg", func(t *testing.T) { 53 | node1, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 54 | "script": "return msg.temperature > 50", 55 | }, registry) 56 | assert.Nil(t, err) 57 | 58 | node2, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 59 | "script": "return msg.temperature > 50", 60 | }, registry) 61 | 62 | node3, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 63 | "script": "return string.upper(msg) == 'AA'", 64 | }, registry) 65 | 66 | node4, err := test.CreateAndInitNode(targetNodeType, types.Configuration{ 67 | "script": "testdata/script.lua", 68 | }, registry) 69 | 70 | metaData := types.BuildMetadata(make(map[string]string)) 71 | metaData.PutValue("productType", "test") 72 | 73 | msg1 := test.Msg{ 74 | MetaData: metaData, 75 | MsgType: "ACTIVITY_EVENT", 76 | Data: "{\"name\":\"aa\",\"temperature\":60,\"humidity\":30}", 77 | AfterSleep: time.Millisecond * 200, 78 | } 79 | msg2 := test.Msg{ 80 | MetaData: metaData, 81 | DataType: types.TEXT, 82 | MsgType: "ACTIVITY_EVENT", 83 | Data: "aa", 84 | AfterSleep: time.Millisecond * 200, 85 | } 86 | var nodeList = []test.NodeAndCallback{ 87 | { 88 | Node: node1, 89 | MsgList: []test.Msg{msg1}, 90 | Callback: func(msg types.RuleMsg, relationType string, err error) { 91 | assert.Equal(t, types.True, relationType) 92 | }, 93 | }, 94 | { 95 | Node: node2, 96 | MsgList: []test.Msg{msg2}, 97 | Callback: func(msg types.RuleMsg, relationType string, err error) { 98 | assert.Equal(t, types.Failure, relationType) 99 | }, 100 | }, 101 | { 102 | Node: node3, 103 | MsgList: []test.Msg{msg2}, 104 | Callback: func(msg types.RuleMsg, relationType string, err error) { 105 | assert.Equal(t, types.True, relationType) 106 | }, 107 | }, 108 | { 109 | Node: node4, 110 | MsgList: []test.Msg{msg1}, 111 | Callback: func(msg types.RuleMsg, relationType string, err error) { 112 | assert.Equal(t, types.True, relationType) 113 | }, 114 | }, 115 | } 116 | for _, item := range nodeList { 117 | test.NodeOnMsgWithChildren(t, item.Node, item.MsgList, item.ChildrenNodes, item.Callback) 118 | } 119 | time.Sleep(time.Millisecond * 20) 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /filter/testdata/script.lua: -------------------------------------------------------------------------------- 1 | -- 定义一个 Filter 函数,接受三个参数:msg, metadata, msgType 2 | function Filter(msg, metadata, msgType) 3 | return msg.temperature > 50 4 | end -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rulego/rulego-components 2 | 3 | go 1.22.7 4 | 5 | require ( 6 | github.com/IBM/sarama v1.43.2 7 | github.com/WuKongIM/WuKongIMGoProto v1.0.8 8 | github.com/WuKongIM/WuKongIMGoSDK v1.0.0 9 | github.com/beanstalkd/go-beanstalk v0.2.0 10 | github.com/expr-lang/expr v1.17.2 11 | github.com/fullstorydev/grpcurl v1.9.1 12 | github.com/golang/protobuf v1.5.4 13 | github.com/jhump/protoreflect v1.16.0 14 | github.com/nats-io/nats.go v1.35.0 15 | github.com/openGemini/opengemini-client-go v0.5.1 16 | github.com/rabbitmq/amqp091-go v1.10.1-0.20240821123418-dc67c21576c2 17 | github.com/redis/go-redis/v9 v9.5.2 18 | github.com/rulego/rulego v0.31.2-0.20250602155557-dd600e38f329 19 | github.com/vadv/gopher-lua-libs v0.5.0 20 | github.com/yuin/gopher-lua v1.1.1 21 | go.mongodb.org/mongo-driver v1.16.1 22 | go.opentelemetry.io/otel v1.33.0 23 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.33.0 24 | go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.33.0 25 | go.opentelemetry.io/otel/metric v1.33.0 26 | go.opentelemetry.io/otel/sdk/metric v1.33.0 27 | google.golang.org/grpc v1.68.1 28 | ) 29 | 30 | require ( 31 | filippo.io/edwards25519 v1.1.0 // indirect 32 | github.com/VividCortex/ewma v1.1.1 // indirect 33 | github.com/alessio/shellescape v1.4.1 // indirect 34 | github.com/aws/aws-sdk-go v1.34.0 // indirect 35 | github.com/beorn7/perks v1.0.1 // indirect 36 | github.com/bufbuild/protocompile v0.10.0 // indirect 37 | github.com/cbroglie/mustache v1.0.1 // indirect 38 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 39 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 40 | github.com/cheggaaa/pb/v3 v3.0.5 // indirect 41 | github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect 42 | github.com/davecgh/go-spew v1.1.1 // indirect 43 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 44 | github.com/dlclark/regexp2 v1.7.0 // indirect 45 | github.com/dop251/goja v0.0.0-20231024180952-594410467bc6 // indirect 46 | github.com/dustin/go-humanize v1.0.0 // indirect 47 | github.com/eapache/go-resiliency v1.6.0 // indirect 48 | github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect 49 | github.com/eapache/queue v1.1.0 // indirect 50 | github.com/eclipse/paho.mqtt.golang v1.4.3 // indirect 51 | github.com/envoyproxy/go-control-plane v0.13.0 // indirect 52 | github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect 53 | github.com/fatih/color v1.7.0 // indirect 54 | github.com/go-logr/logr v1.4.2 // indirect 55 | github.com/go-logr/stdr v1.2.2 // indirect 56 | github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect 57 | github.com/go-sql-driver/mysql v1.8.1 // indirect 58 | github.com/gofrs/uuid/v5 v5.0.0 // indirect 59 | github.com/golang/snappy v0.0.4 // indirect 60 | github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect 61 | github.com/google/uuid v1.6.0 // indirect 62 | github.com/gorilla/websocket v1.5.0 // indirect 63 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 // indirect 64 | github.com/hashicorp/errwrap v1.0.0 // indirect 65 | github.com/hashicorp/go-multierror v1.1.1 // indirect 66 | github.com/hashicorp/go-uuid v1.0.3 // indirect 67 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 68 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 69 | github.com/jcmturner/gofork v1.7.6 // indirect 70 | github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect 71 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 72 | github.com/jmespath/go-jmespath v0.3.0 // indirect 73 | github.com/julienschmidt/httprouter v1.3.0 // indirect 74 | github.com/klauspost/compress v1.17.8 // indirect 75 | github.com/lib/pq v1.10.9 // indirect 76 | github.com/mattn/go-colorable v0.1.2 // indirect 77 | github.com/mattn/go-isatty v0.0.12 // indirect 78 | github.com/mattn/go-runewidth v0.0.7 // indirect 79 | github.com/mattn/go-sqlite3 v1.14.7 // indirect 80 | github.com/mitchellh/mapstructure v1.5.0 // indirect 81 | github.com/montanaflynn/stats v0.7.1 // indirect 82 | github.com/nats-io/nkeys v0.4.7 // indirect 83 | github.com/nats-io/nuid v1.0.1 // indirect 84 | github.com/pierrec/lz4/v4 v4.1.21 // indirect 85 | github.com/pkg/errors v0.9.1 // indirect 86 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 87 | github.com/pmezard/go-difflib v1.0.0 // indirect 88 | github.com/prometheus/client_golang v1.19.1 // indirect 89 | github.com/prometheus/client_model v0.6.0 // indirect 90 | github.com/prometheus/common v0.48.0 // indirect 91 | github.com/prometheus/procfs v0.12.0 // indirect 92 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 93 | github.com/robfig/cron/v3 v3.0.1 // indirect 94 | github.com/stretchr/testify v1.10.0 // indirect 95 | github.com/technoweenie/multipartstreamer v1.0.1 // indirect 96 | github.com/valyala/bytebufferpool v1.0.0 // indirect 97 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect 98 | github.com/xdg-go/scram v1.1.2 // indirect 99 | github.com/xdg-go/stringprep v1.0.4 // indirect 100 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect 101 | github.com/yuin/gluamapper v0.0.0-20150323120927-d836955830e7 // indirect 102 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 103 | go.opentelemetry.io/otel/sdk v1.33.0 // indirect 104 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 105 | go.opentelemetry.io/proto/otlp v1.4.0 // indirect 106 | go.uber.org/atomic v1.11.0 // indirect 107 | golang.org/x/crypto v0.30.0 // indirect 108 | golang.org/x/net v0.32.0 // indirect 109 | golang.org/x/sync v0.10.0 // indirect 110 | golang.org/x/sys v0.28.0 // indirect 111 | golang.org/x/text v0.21.0 // indirect 112 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect 113 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 114 | google.golang.org/protobuf v1.35.2 // indirect 115 | gopkg.in/xmlpath.v2 v2.0.0-20150820204837-860cbeca3ebc // indirect 116 | gopkg.in/yaml.v2 v2.4.0 // indirect 117 | gopkg.in/yaml.v3 v3.0.1 // indirect 118 | ) 119 | 120 | //replace github.com/rulego/rulego => ../rulego 121 | -------------------------------------------------------------------------------- /pkg/lua_engine/tools.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package luaEngine 18 | 19 | import ( 20 | "github.com/yuin/gopher-lua" 21 | "reflect" 22 | "strings" 23 | ) 24 | 25 | // StringMapToLTable converts a map to a lua.LTable 26 | func StringMapToLTable(L *lua.LState, m map[string]string) *lua.LTable { 27 | // create a new lua.LTable 28 | table := L.NewTable() 29 | // iterate over the map 30 | for k, v := range m { 31 | // convert the key to a lua.LString 32 | lk := lua.LString(k) 33 | // convert the value to a lua.LValue 34 | lv := lua.LString(v) 35 | // set the key-value pair to the table 36 | table.RawSet(lk, lv) 37 | } 38 | return table 39 | } 40 | 41 | // MapToLTable converts a map to a lua.LTable 42 | func MapToLTable(L *lua.LState, m map[string]interface{}) *lua.LTable { 43 | // create a new lua.LTable 44 | table := L.NewTable() 45 | // iterate over the map 46 | for k, v := range m { 47 | // convert the key to a lua.LString 48 | lk := lua.LString(k) 49 | // convert the value to a lua.LValue 50 | lv := GoToLua(L, v) 51 | // set the key-value pair to the table 52 | table.RawSet(lk, lv) 53 | } 54 | return table 55 | } 56 | 57 | // GoToLua converts a Go value to a lua.LValue 58 | func GoToLua(L *lua.LState, v interface{}) lua.LValue { 59 | // get the value's type and kind 60 | t := reflect.TypeOf(v) 61 | k := t.Kind() 62 | // switch on the kind 63 | switch k { 64 | case reflect.String: 65 | // convert string to lua.LString 66 | return lua.LString(v.(string)) 67 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 68 | // convert int to lua.LNumber 69 | return lua.LNumber(v.(int)) 70 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 71 | // convert uint to lua.LNumber 72 | return lua.LNumber(v.(uint)) 73 | case reflect.Float32, reflect.Float64: 74 | // convert float to lua.LNumber 75 | return lua.LNumber(v.(float64)) 76 | case reflect.Bool: 77 | // convert bool to lua.LBool 78 | return lua.LBool(v.(bool)) 79 | //case reflect.Slice: 80 | // // convert slice to lua.LTable 81 | // return SliceToLTable(L, v) 82 | case reflect.Map: 83 | // convert map to lua.LTable 84 | return MapToLTable(L, v.(map[string]interface{})) 85 | //case reflect.Struct: 86 | // // convert struct to lua.LTable 87 | // return StructToLTable(L, v) 88 | default: 89 | // return nil for unsupported types 90 | return lua.LNil 91 | } 92 | } 93 | 94 | // LTableToStringMap converts a lua.LTable to a map[string]string 95 | func LTableToStringMap(table *lua.LTable) map[string]string { 96 | // create a new map[string]string 97 | m := make(map[string]string) 98 | // iterate over the table 99 | table.ForEach(func(key lua.LValue, value lua.LValue) { 100 | // convert the key and value to string 101 | k := key.String() 102 | v := value.String() 103 | // set the key-value pair to the map 104 | m[k] = v 105 | }) 106 | return m 107 | } 108 | 109 | // LTableToMap converts a lua.LTable to a map[string]interface{} 110 | func LTableToMap(table *lua.LTable) map[string]interface{} { 111 | // create a new map[string]interface{} 112 | m := make(map[string]interface{}) 113 | // iterate over the table 114 | table.ForEach(func(key lua.LValue, value lua.LValue) { 115 | // convert the key to string 116 | k := key.String() 117 | // convert the value to interface{} 118 | v := LuaToGo(value) 119 | // set the key-value pair to the map 120 | m[k] = v 121 | }) 122 | return m 123 | } 124 | 125 | // LuaToGo converts a lua.LValue to an interface{} 126 | func LuaToGo(value lua.LValue) interface{} { 127 | // switch on the value type 128 | switch value.Type() { 129 | case lua.LTNil: 130 | // return nil for nil values 131 | return nil 132 | case lua.LTBool: 133 | // return bool for boolean values 134 | return bool(value.(lua.LBool)) 135 | case lua.LTNumber: 136 | // return float64 for number values 137 | return float64(value.(lua.LNumber)) 138 | case lua.LTString: 139 | // return string for string values 140 | return string(value.(lua.LString)) 141 | case lua.LTTable: 142 | // return map[string]interface{} for table values 143 | return LTableToMap(value.(*lua.LTable)) 144 | default: 145 | // return nil for unsupported types 146 | return nil 147 | } 148 | } 149 | 150 | // ValidateLua 验证脚本是否正确 151 | func ValidateLua(script string) error { 152 | L := lua.NewState() 153 | if strings.HasSuffix(script, ".lua") { 154 | return L.DoFile(script) 155 | } else { 156 | return L.DoString(script) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /stats/README.md: -------------------------------------------------------------------------------- 1 | # stats 2 | Perform statistics and analysis on data. 3 | 4 | ## How to customize components 5 | Implement the `types.Node` interface. 6 | Example of a custom component: 7 | ```go 8 | // Define the Node component 9 | // UpperNode A plugin that converts the message data to uppercase 10 | type UpperNode struct{} 11 | 12 | func (n *UpperNode) Type() string { 13 | return "test/upper" 14 | } 15 | 16 | func (n *UpperNode) New() types.Node { 17 | return &UpperNode{} 18 | } 19 | 20 | func (n *UpperNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 21 | // Do some initialization work 22 | return nil 23 | } 24 | 25 | // Process the message 26 | func (n *UpperNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 27 | msg.Data = strings.ToUpper(msg.Data) 28 | // Send the modified message to the next node 29 | ctx.TellSuccess(msg) 30 | return nil 31 | } 32 | 33 | func (n *UpperNode) Destroy() { 34 | // Do some cleanup work 35 | } 36 | ``` 37 | 38 | ## Usage 39 | 40 | Register the component to the `RuleGo` default registry: 41 | ```go 42 | rulego.Registry.Register(&MyNode{}) 43 | ``` 44 | 45 | Then use your component in the rule chain DSL file: 46 | ```json 47 | { 48 | "ruleChain": { 49 | "id": "rue01", 50 | "name": "Test rule chain" 51 | }, 52 | "metadata": { 53 | "nodes": [ 54 | { 55 | "id": "s1", 56 | "type": "test/upper", 57 | "name": "Name", 58 | "debugMode": true, 59 | "configuration": { 60 | "field1": "Configuration parameters defined by the component", 61 | "....": "..." 62 | } 63 | } 64 | ], 65 | "connections": [ 66 | { 67 | "fromId": "s1", 68 | "toId": "Connect to the next component ID", 69 | "type": "The connection relationship with the component" 70 | } 71 | ] 72 | } 73 | } 74 | ``` -------------------------------------------------------------------------------- /stats/README_ZH.md: -------------------------------------------------------------------------------- 1 | # stats 2 | 对数据进行统计、分析。 3 | 4 | ## 怎样自定义组件 5 | 实现`types.Node`接口,例子: 6 | ```go 7 | //定义Node组件 8 | //UpperNode A plugin that converts the message data to uppercase 9 | type UpperNode struct{} 10 | 11 | func (n *UpperNode) Type() string { 12 | return "test/upper" 13 | } 14 | func (n *UpperNode) New() types.Node { 15 | return &UpperNode{} 16 | } 17 | func (n *UpperNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 18 | // Do some initialization work 19 | return nil 20 | } 21 | //处理消息 22 | func (n *UpperNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 23 | msg.Data = strings.ToUpper(msg.Data) 24 | // Send the modified message to the next node 25 | ctx.TellSuccess(msg) 26 | return nil 27 | } 28 | 29 | func (n *UpperNode) Destroy() { 30 | // Do some cleanup work 31 | } 32 | ``` 33 | 34 | ## 使用 35 | 36 | 把组件注册到`RuleGo`默认注册器 37 | ```go 38 | rulego.Registry.Register(&MyNode{}) 39 | ``` 40 | 41 | 然后在规则链DSL文件使用您的组件 42 | ```json 43 | { 44 | "ruleChain": { 45 | "id": "rue01", 46 | "name": "测试规则链", 47 | }, 48 | "metadata": { 49 | "nodes": [ 50 | { 51 | "id": "s1", 52 | "type": "test/upper", 53 | "name": "名称", 54 | "debugMode": true, 55 | "configuration": { 56 | "field1": "组件定义的配置参数", 57 | "....": "..." 58 | } 59 | } 60 | ], 61 | "connections": [ 62 | { 63 | "fromId": "s1", 64 | "toId": "连接下一个组件ID", 65 | "type": "与组件的连接关系" 66 | } 67 | ] 68 | } 69 | } 70 | ``` -------------------------------------------------------------------------------- /testdata/chain_call_rest_api.json: -------------------------------------------------------------------------------- 1 | { 2 | "ruleChain": { 3 | "id":"chain_call_rest_api", 4 | "name": "测试规则链", 5 | "root": true 6 | }, 7 | "metadata": { 8 | "nodes": [ 9 | { 10 | "id": "s1", 11 | "type": "jsFilter", 12 | "name": "过滤", 13 | "debugMode": true, 14 | "configuration": { 15 | "jsScript": "return msg!='bb';" 16 | } 17 | }, 18 | { 19 | "id": "s2", 20 | "type": "jsTransform", 21 | "name": "转换", 22 | "debugMode": true, 23 | "configuration": { 24 | "jsScript": "metadata['test']='test02';\n metadata['index']=52;\n msgType='TEST_MSG_TYPE2';\n msg['aa']=66; return {'msg':msg,'metadata':metadata,'msgType':msgType};" 25 | } 26 | }, 27 | { 28 | "id": "s3", 29 | "type": "restApiCall", 30 | "name": "推送数据", 31 | "debugMode": true, 32 | "configuration": { 33 | "restEndpointUrlPattern": "http://192.168.136.26:9099/api/msg", 34 | "requestMethod": "POST", 35 | "maxParallelRequestsCount": 200 36 | } 37 | } 38 | ], 39 | "connections": [ 40 | { 41 | "fromId": "s1", 42 | "toId": "s2", 43 | "type": "True" 44 | }, 45 | { 46 | "fromId": "s2", 47 | "toId": "s3", 48 | "type": "Success" 49 | } 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /testdata/chain_msg_type_switch.json: -------------------------------------------------------------------------------- 1 | { 2 | "ruleChain": { 3 | "id":"chain_msg_type_switch", 4 | "name": "测试规则链-msgTypeSwitch", 5 | "root": true 6 | }, 7 | "metadata": { 8 | "nodes": [ 9 | { 10 | "id": "s1", 11 | "type": "msgTypeSwitch", 12 | "name": "消息路由", 13 | "debugMode": true 14 | }, 15 | { 16 | "id": "s2", 17 | "type": "log", 18 | "name": "记录日志1", 19 | "debugMode": true, 20 | "configuration": { 21 | "jsScript": "return msgType+':s2';" 22 | } 23 | }, 24 | { 25 | "id": "s3", 26 | "type": "log", 27 | "name": "记录日志2", 28 | "debugMode": true, 29 | "configuration": { 30 | "jsScript": "return msgType+':s3';" 31 | } 32 | }, 33 | { 34 | "id": "s4", 35 | "type": "log", 36 | "name": "记录日志3", 37 | "debugMode": true, 38 | "configuration": { 39 | "jsScript": "return msgType+':s4';" 40 | } 41 | } 42 | ], 43 | "connections": [ 44 | { 45 | "fromId": "s1", 46 | "toId": "s2", 47 | "type": "TEST_MSG_TYPE1" 48 | }, 49 | { 50 | "fromId": "s1", 51 | "toId": "s3", 52 | "type": "TEST_MSG_TYPE1" 53 | }, 54 | { 55 | "fromId": "s1", 56 | "toId": "s4", 57 | "type": "TEST_MSG_TYPE2" 58 | } 59 | ] 60 | } 61 | } -------------------------------------------------------------------------------- /testdata/filter_node.json: -------------------------------------------------------------------------------- 1 | { 2 | "ruleChain": { 3 | "additionalInfo": { 4 | "description": "" 5 | }, 6 | "name": "测试规则链", 7 | "firstRuleNodeId": null, 8 | "root": false, 9 | "debugMode": false, 10 | "configuration": null 11 | }, 12 | "metadata": { 13 | "firstNodeIndex": 0, 14 | "nodes": [ 15 | { 16 | "type": "jsFilter", 17 | "name": "过滤", 18 | "debugMode": true, 19 | "configuration": { 20 | "jsScript": "return msg.temperature>10;" 21 | } 22 | } 23 | ], 24 | "connections": [ 25 | 26 | ] 27 | } 28 | } -------------------------------------------------------------------------------- /testdata/not_debug_mode_chain.json: -------------------------------------------------------------------------------- 1 | { 2 | "ruleChain": { 3 | "id":"not_debug_mode_chain", 4 | "name": "测试规则链", 5 | "root": true 6 | }, 7 | "metadata": { 8 | "nodes": [ 9 | { 10 | "id": "s1", 11 | "type": "jsFilter", 12 | "name": "过滤", 13 | "debugMode": false, 14 | "configuration": { 15 | "jsScript": "return msg!='aa';" 16 | } 17 | }, 18 | { 19 | "id": "s2", 20 | "type": "jsTransform", 21 | "name": "转换", 22 | "debugMode": false, 23 | "configuration": { 24 | "jsScript": "metadata['test']='test02';\n metadata['index']=52;\n msgType='TEST_MSG_TYPE2';var msg2={};\n msg2['bb']=22\n return {'msg':msg2,'metadata':metadata,'msgType':msgType};" 25 | } 26 | } 27 | ], 28 | "connections": [ 29 | { 30 | "fromId": "s1", 31 | "toId": "s2", 32 | "type": "True" 33 | } 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /testdata/test_context_chain.json: -------------------------------------------------------------------------------- 1 | { 2 | "ruleChain": { 3 | "id":"test_context_chain", 4 | "name": "测试数据共享规则链" 5 | }, 6 | "metadata": { 7 | "nodes": [ 8 | { 9 | "id": "s1", 10 | "type": "test/upper", 11 | "name": "test upper", 12 | "debugMode": false 13 | }, 14 | { 15 | "id":"s2", 16 | "type": "test/time", 17 | "name": "add time", 18 | "debugMode": false 19 | } 20 | ], 21 | "connections": [ 22 | { 23 | "fromId": "s1", 24 | "toId": "s2", 25 | "type": "Success" 26 | } 27 | ] 28 | } 29 | } -------------------------------------------------------------------------------- /transform/README.md: -------------------------------------------------------------------------------- 1 | # transform 2 | Transform messages. 3 | 4 | ## How to customize components 5 | Implement the `types.Node` interface. 6 | Example of a custom component: 7 | ```go 8 | // Define the Node component 9 | // UpperNode A plugin that converts the message data to uppercase 10 | type UpperNode struct{} 11 | 12 | func (n *UpperNode) Type() string { 13 | return "test/upper" 14 | } 15 | 16 | func (n *UpperNode) New() types.Node { 17 | return &UpperNode{} 18 | } 19 | 20 | func (n *UpperNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 21 | // Do some initialization work 22 | return nil 23 | } 24 | 25 | // Process the message 26 | func (n *UpperNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 27 | msg.Data = strings.ToUpper(msg.Data) 28 | // Send the modified message to the next node 29 | ctx.TellSuccess(msg) 30 | return nil 31 | } 32 | 33 | func (n *UpperNode) Destroy() { 34 | // Do some cleanup work 35 | } 36 | ``` 37 | 38 | ## Usage 39 | 40 | Register the component to the `RuleGo` default registry: 41 | ```go 42 | rulego.Registry.Register(&MyNode{}) 43 | ``` 44 | 45 | Then use your component in the rule chain DSL file: 46 | ```json 47 | { 48 | "ruleChain": { 49 | "id": "rue01", 50 | "name": "Test rule chain 51 | }, 52 | "metadata": { 53 | "nodes": [ 54 | { 55 | "id": "s1", 56 | "type": "test/upper", 57 | "name": "Name", 58 | "debugMode": true, 59 | "configuration": { 60 | "field1": "Configuration parameters defined by the component", 61 | "....": "..." 62 | } 63 | } 64 | ], 65 | "connections": [ 66 | { 67 | "fromId": "s1", 68 | "toId": "Connect to the next component ID", 69 | "type": "The connection relationship with the component" 70 | } 71 | ] 72 | } 73 | } 74 | ``` -------------------------------------------------------------------------------- /transform/README_ZH.md: -------------------------------------------------------------------------------- 1 | # transform 2 | 对消息进行转换。 3 | 4 | ## 怎样自定义组件 5 | 实现`types.Node`接口,例子: 6 | ```go 7 | //定义Node组件 8 | //UpperNode A plugin that converts the message data to uppercase 9 | type UpperNode struct{} 10 | 11 | func (n *UpperNode) Type() string { 12 | return "test/upper" 13 | } 14 | func (n *UpperNode) New() types.Node { 15 | return &UpperNode{} 16 | } 17 | func (n *UpperNode) Init(ruleConfig types.Config, configuration types.Configuration) error { 18 | // Do some initialization work 19 | return nil 20 | } 21 | //处理消息 22 | func (n *UpperNode) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 23 | msg.Data = strings.ToUpper(msg.Data) 24 | // Send the modified message to the next node 25 | ctx.TellSuccess(msg) 26 | return nil 27 | } 28 | 29 | func (n *UpperNode) Destroy() { 30 | // Do some cleanup work 31 | } 32 | ``` 33 | 34 | ## 使用 35 | 36 | 把组件注册到`RuleGo`默认注册器 37 | ```go 38 | rulego.Registry.Register(&MyNode{}) 39 | ``` 40 | 41 | 然后在规则链DSL文件使用您的组件 42 | ```json 43 | { 44 | "ruleChain": { 45 | "id": "rue01", 46 | "name": "测试规则链" 47 | }, 48 | "metadata": { 49 | "nodes": [ 50 | { 51 | "id": "s1", 52 | "type": "test/upper", 53 | "name": "名称", 54 | "debugMode": true, 55 | "configuration": { 56 | "field1": "组件定义的配置参数", 57 | "....": "..." 58 | } 59 | } 60 | ], 61 | "connections": [ 62 | { 63 | "fromId": "s1", 64 | "toId": "连接下一个组件ID", 65 | "type": "与组件的连接关系" 66 | } 67 | ] 68 | } 69 | } 70 | ``` -------------------------------------------------------------------------------- /transform/lua_transform.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "github.com/rulego/rulego" 23 | "github.com/rulego/rulego-components/pkg/lua_engine" 24 | "github.com/rulego/rulego/api/types" 25 | "github.com/rulego/rulego/utils/maps" 26 | "github.com/yuin/gopher-lua" 27 | "strings" 28 | ) 29 | 30 | // init registers the component to rulego 31 | func init() { 32 | _ = rulego.Registry.Register(&LuaTransform{}) 33 | } 34 | 35 | // LuaTransformConfiguration node configuration 36 | type LuaTransformConfiguration struct { 37 | //Script configures the function body content or the script file path with `.lua` as the suffix 38 | //Only need to provide the function body content, if it is a file path, then need to provide the complete script function: 39 | //function Transform(msg, metadata, msgType) ${Script} \n end 40 | //return msg, metadata, msgType 41 | //The parameter msg, if the data type of msg is JSON, then it will be converted to the Lua table type before calling the function 42 | Script string 43 | } 44 | 45 | // LuaTransform is a component that transforms messages based on Lua scripts 46 | type LuaTransform struct { 47 | Config LuaTransformConfiguration 48 | // pool is a sync.Pool of *lua.LState 49 | pool *luaEngine.LStatePool 50 | } 51 | 52 | // New creates a new instance of LuaFilter 53 | func (x *LuaTransform) New() types.Node { 54 | return &LuaTransform{Config: LuaTransformConfiguration{ 55 | Script: "return msg, metadata, msgType", 56 | }} 57 | } 58 | 59 | // Type returns the type of the component 60 | func (x *LuaTransform) Type() string { 61 | return "x/luaTransform" 62 | } 63 | 64 | // Init initializes the component 65 | func (x *LuaTransform) Init(ruleConfig types.Config, configuration types.Configuration) error { 66 | err := maps.Map2Struct(configuration, &x.Config) 67 | if err == nil { 68 | 69 | if strings.HasSuffix(x.Config.Script, ".lua") { 70 | if err = luaEngine.ValidateLua(x.Config.Script); err != nil { 71 | return err 72 | } 73 | // create a new LStatePool from file 74 | x.pool = luaEngine.NewFileLStatePool(ruleConfig, x.Config.Script, configuration) 75 | } else { 76 | script := fmt.Sprintf("function Transform(msg, metadata, msgType) %s \nend", x.Config.Script) 77 | if err = luaEngine.ValidateLua(script); err != nil { 78 | return err 79 | } 80 | // create a new LStatePool from script 81 | x.pool = luaEngine.NewStringLStatePool(ruleConfig, script, configuration) 82 | } 83 | 84 | } 85 | return err 86 | } 87 | 88 | // OnMsg handles the message 89 | func (x *LuaTransform) OnMsg(ctx types.RuleContext, msg types.RuleMsg) { 90 | // get a *lua.LState from the pool 91 | L := x.pool.Get() 92 | if L == nil { 93 | // if there is no available *lua.LState, tell the next node to fail 94 | ctx.TellFailure(msg, fmt.Errorf("x/luaTransform lua.LState nil error")) 95 | return 96 | } 97 | // defer putting back the *lua.LState to the pool 98 | defer x.pool.Put(L) 99 | 100 | //var data interface{} = msg.Data 101 | var dataMap map[string]interface{} 102 | if msg.DataType == types.JSON { 103 | _ = json.Unmarshal([]byte(msg.GetData()), &dataMap) 104 | } 105 | var err error 106 | transform := L.GetGlobal("Transform") 107 | p := lua.P{ 108 | Fn: transform, 109 | NRet: 3, // Specify the number of return values 110 | Protect: true, 111 | } 112 | if dataMap != nil { 113 | // Call the Transform function, passing in msg, metadata, msgType as arguments. 114 | err = L.CallByParam(p, luaEngine.MapToLTable(L, dataMap), luaEngine.StringMapToLTable(L, msg.Metadata.Values()), lua.LString(msg.Type)) 115 | } else { 116 | // Call the Transform function, passing in msg, metadata, msgType as arguments. 117 | err = L.CallByParam(p, lua.LString(msg.GetData()), luaEngine.StringMapToLTable(L, msg.Metadata.Values()), lua.LString(msg.Type)) 118 | } 119 | 120 | if err != nil { 121 | // if there is an error, tell the next node to fail 122 | ctx.TellFailure(msg, err) 123 | return 124 | } 125 | // get the return values from the script 126 | ret1 := L.Get(-3) // msg 127 | ret2 := L.Get(-2) // metadata 128 | ret3 := L.Get(-1) // msgType 129 | // pop the values from the stack 130 | L.Pop(3) 131 | 132 | // update the msg fields with the new values 133 | // if newMsg is a lua.LTable type value, it means a JSON string 134 | if newMsg, ok := ret1.(*lua.LTable); ok { 135 | // Convert newMsg to a map[string]interface{} type value 136 | newMsgMap := luaEngine.LTableToMap(newMsg) 137 | // Convert dataMap to a JSON format string and assign it to msg.Data 138 | if b, err := json.Marshal(newMsgMap); err != nil { 139 | ctx.TellFailure(msg, err) 140 | return 141 | } else { 142 | msg.SetData(string(b)) 143 | } 144 | } else if newMsgString, ok := ret1.(lua.LString); ok { 145 | // If newMsg is not a lua.LTable type value, it means a normal string 146 | //Directly convert newMsg to a string type value and assign it to msg.Data 147 | msg.SetData(string(newMsgString)) 148 | } 149 | 150 | // If newMetadata is a lua.LTable type value, it means a metadata table 151 | if newMetadata, ok := ret2.(*lua.LTable); ok { 152 | // Convert newMetadata to a map[string]string type value and assign it to msg.Metadata 153 | msg.Metadata.ReplaceAll(luaEngine.LTableToStringMap(newMetadata)) 154 | } else { 155 | // If newMetadata is not a lua.LTable type value, it means a nil value 156 | // Do not modify the value of msg.Metadata 157 | } 158 | // If newMsgType is a lua.LString type value, it means a message type string 159 | if newMsgType, ok := ret3.(lua.LString); ok { 160 | // Convert newMsgType to a string type value and assign it to msg.Type 161 | msg.Type = string(newMsgType) 162 | } else { 163 | // If newMsgType is not a lua.LString type value, it means a nil value 164 | // Do not modify the value of msg.Type 165 | } 166 | 167 | ctx.TellSuccess(msg) 168 | } 169 | 170 | // Destroy releases the resources of the component 171 | func (x *LuaTransform) Destroy() { 172 | if x.pool != nil { 173 | x.pool.Shutdown() 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /transform/lua_transform_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The RuleGo Authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package transform 18 | 19 | import ( 20 | luaEngine "github.com/rulego/rulego-components/pkg/lua_engine" 21 | "github.com/rulego/rulego/api/types" 22 | "github.com/rulego/rulego/components/transform" 23 | "github.com/rulego/rulego/test" 24 | "github.com/rulego/rulego/test/assert" 25 | lua "github.com/yuin/gopher-lua" 26 | "sync" 27 | "testing" 28 | "time" 29 | ) 30 | 31 | func TestLuaTransform(t *testing.T) { 32 | var targetNodeType = "x/luaTransform" 33 | var registry = &types.SafeComponentSlice{} 34 | registry.Add(&LuaTransform{}) 35 | 36 | t.Run("NewNode", func(t *testing.T) { 37 | test.NodeNew(t, targetNodeType, &LuaTransform{}, types.Configuration{}, registry) 38 | }) 39 | 40 | t.Run("InitNode", func(t *testing.T) { 41 | test.NodeInit(t, targetNodeType, types.Configuration{ 42 | "script": "return msg, metadata, msgType", 43 | }, types.Configuration{ 44 | "script": "return msg, metadata, msgType", 45 | }, registry) 46 | }) 47 | 48 | t.Run("DefaultConfig", func(t *testing.T) { 49 | test.NodeInit(t, targetNodeType, types.Configuration{ 50 | "script": "return msg, metadata, msgType", 51 | }, types.Configuration{ 52 | "script": "return msg, metadata, msgType", 53 | }, registry) 54 | }) 55 | 56 | config := types.NewConfig() 57 | config.Properties.PutValue("from", "test") 58 | // 定义一个 Go 函数,接受两个数字参数,返回它们的和 59 | config.RegisterUdf("add", types.Script{Type: types.Lua, Content: func(L *lua.LState) int { 60 | a := L.CheckNumber(1) 61 | b := L.CheckNumber(2) 62 | L.Push(lua.LNumber(a + b)) 63 | return 1 64 | }}) 65 | 66 | config.RegisterUdf("add", types.Script{Type: types.Js, Content: func(a, b int) int { 67 | return a + b 68 | }}) 69 | 70 | //注册第三方lua工具库 71 | config.Properties.PutValue(luaEngine.LoadLuaLibs, "true") 72 | //luaEngine.Preloader.Register(func(state *lua.LState) { 73 | // libs.Preload(state) 74 | //}) 75 | 76 | factory := &LuaTransform{} 77 | 78 | t.Run("OnMsg", func(t *testing.T) { 79 | //测试自定义函数是否影响js运行时 80 | jsFactory := &transform.JsTransformNode{} 81 | jsNode := jsFactory.New() 82 | err := jsNode.Init(config, types.Configuration{ 83 | "jsScript": ` 84 | metadata.from = global.from 85 | metadata.add = add(5,4) 86 | return {'msg':msg, 'metadata':metadata, 'msgType':msgType} 87 | `, 88 | }) 89 | assert.Nil(t, err) 90 | 91 | node1 := factory.New() 92 | err = node1.Init(config, types.Configuration{ 93 | "script": ` 94 | -- 将温度值从摄氏度转换为华氏度 95 | msg.temperature = msg.temperature * 1.8 + 32 96 | -- 在 metadata 中添加一个字段,表示温度单位 97 | metadata.unit = "F" 98 | metadata.from = global.from 99 | metadata.add = add(5,4) 100 | return msg, metadata, msgType 101 | `, 102 | }) 103 | assert.Nil(t, err) 104 | 105 | node2 := factory.New() 106 | err = node2.Init(config, types.Configuration{ 107 | "script": "return string.upper(msg), metadata, msgType", 108 | }) 109 | 110 | node3 := factory.New() 111 | err = node3.Init(config, types.Configuration{ 112 | "script": "testdata/script.lua", 113 | }) 114 | node4 := factory.New() 115 | err = node4.Init(config, types.Configuration{ 116 | "script": "testdata/libs_script.lua", 117 | }) 118 | 119 | metaData := types.BuildMetadata(make(map[string]string)) 120 | metaData.PutValue("productType", "test") 121 | 122 | msg1 := test.Msg{ 123 | MetaData: metaData, 124 | MsgType: "ACTIVITY_EVENT", 125 | Data: "{\"name\":\"aa\",\"temperature\":60,\"humidity\":30}", 126 | AfterSleep: time.Millisecond * 200, 127 | } 128 | msg2 := test.Msg{ 129 | MetaData: metaData, 130 | DataType: types.TEXT, 131 | MsgType: "ACTIVITY_EVENT", 132 | Data: "aa", 133 | AfterSleep: time.Millisecond * 200, 134 | } 135 | var nodeList = []test.NodeAndCallback{ 136 | { 137 | Node: node1, 138 | MsgList: []test.Msg{msg1}, 139 | Callback: func(msg types.RuleMsg, relationType string, err error) { 140 | assert.Equal(t, types.Success, relationType) 141 | assert.Equal(t, "F", msg.Metadata.GetValue("unit")) 142 | assert.Equal(t, "test", msg.Metadata.GetValue("from")) 143 | assert.Equal(t, "9", msg.Metadata.GetValue("add")) 144 | assert.Equal(t, "{\"humidity\":30,\"name\":\"aa\",\"temperature\":140}", msg.Data) 145 | }, 146 | }, 147 | { 148 | Node: node2, 149 | MsgList: []test.Msg{msg2}, 150 | Callback: func(msg types.RuleMsg, relationType string, err error) { 151 | assert.Equal(t, types.Success, relationType) 152 | assert.Equal(t, "AA", msg.Data) 153 | }, 154 | }, 155 | { 156 | Node: node3, 157 | MsgList: []test.Msg{msg1}, 158 | Callback: func(msg types.RuleMsg, relationType string, err error) { 159 | assert.Equal(t, types.Success, relationType) 160 | assert.Equal(t, "F", msg.Metadata.GetValue("unit")) 161 | assert.Equal(t, "{\"humidity\":30,\"name\":\"aa\",\"temperature\":140}", msg.Data) 162 | }, 163 | }, 164 | { 165 | Node: node4, 166 | MsgList: []test.Msg{msg1}, 167 | Callback: func(msg types.RuleMsg, relationType string, err error) { 168 | assert.Equal(t, types.Success, relationType) 169 | assert.Equal(t, "b026324c6904b2a9cb4b88d6d61c81d1", msg.Metadata.GetValue("md5")) 170 | }, 171 | }, 172 | { 173 | Node: jsNode, 174 | MsgList: []test.Msg{msg1}, 175 | Callback: func(msg types.RuleMsg, relationType string, err error) { 176 | assert.Equal(t, types.Success, relationType) 177 | assert.Equal(t, "test", msg.Metadata.GetValue("from")) 178 | assert.Equal(t, "9", msg.Metadata.GetValue("add")) 179 | }, 180 | }, 181 | } 182 | for _, item := range nodeList { 183 | test.NodeOnMsgWithChildren(t, item.Node, item.MsgList, item.ChildrenNodes, item.Callback) 184 | } 185 | time.Sleep(time.Millisecond * 20) 186 | }) 187 | 188 | t.Run("OnMsgConcurrency", func(t *testing.T) { 189 | node1 := factory.New() 190 | err := node1.Init(config, types.Configuration{ 191 | "script": ` 192 | -- 将温度值从摄氏度转换为华氏度 193 | msg.temperature = msg.temperature * 1.8 + 32 194 | -- 在 metadata 中添加一个字段,表示温度单位 195 | metadata.unit = "F" 196 | metadata.from = global.from 197 | metadata.add = add(5,4) 198 | return msg, metadata, msgType 199 | `, 200 | }) 201 | assert.Nil(t, err) 202 | var i = 0 203 | msg1 := types.NewMsg(time.Now().UnixMilli(), "ACTIVITY_EVENT", types.JSON, types.NewMetadata(), "{\"name\":\"aa\",\"temperature\":60,\"humidity\":30}") 204 | msg2 := types.NewMsg(time.Now().UnixMilli(), "ACTIVITY_EVENT", types.JSON, types.NewMetadata(), "{\"name\":\"aa\",\"temperature\":70,\"humidity\":30}") 205 | 206 | var wg = sync.WaitGroup{} 207 | wg.Add(100) 208 | ctx1 := test.NewRuleContextFull(config, node1, nil, func(msg types.RuleMsg, relationType string, err error) { 209 | assert.Equal(t, types.Success, relationType) 210 | assert.Equal(t, "F", msg.Metadata.GetValue("unit")) 211 | assert.Equal(t, "test", msg.Metadata.GetValue("from")) 212 | assert.Equal(t, "9", msg.Metadata.GetValue("add")) 213 | assert.Equal(t, "{\"humidity\":30,\"name\":\"aa\",\"temperature\":140}", msg.Data) 214 | wg.Done() 215 | }) 216 | ctx2 := test.NewRuleContextFull(config, node1, nil, func(msg types.RuleMsg, relationType string, err error) { 217 | assert.Equal(t, types.Success, relationType) 218 | assert.Equal(t, "F", msg.Metadata.GetValue("unit")) 219 | assert.Equal(t, "test", msg.Metadata.GetValue("from")) 220 | assert.Equal(t, "9", msg.Metadata.GetValue("add")) 221 | assert.Equal(t, "{\"humidity\":30,\"name\":\"aa\",\"temperature\":158}", msg.Data) 222 | wg.Done() 223 | }) 224 | for i < 100 { 225 | if i%2 == 0 { 226 | go node1.OnMsg(ctx1, msg1) 227 | 228 | } else { 229 | go node1.OnMsg(ctx2, msg2) 230 | } 231 | i++ 232 | } 233 | wg.Wait() 234 | }) 235 | } 236 | -------------------------------------------------------------------------------- /transform/testdata/libs_script.lua: -------------------------------------------------------------------------------- 1 | function Transform(msg, metadata, msgType) 2 | local crypto = require("crypto") 3 | -- md5 4 | metadata.md5 = crypto.md5("1\n") 5 | -- 返回修改后的 msg, metadata, msgType 6 | return msg, metadata, msgType 7 | end 8 | -------------------------------------------------------------------------------- /transform/testdata/script.lua: -------------------------------------------------------------------------------- 1 | -- 定义一个 Transform 函数,接受三个参数:msg, metadata, msgType 2 | -- 根据 msg, metadata, msgType 来转换消息,返回修改后的值 3 | function Transform(msg, metadata, msgType) 4 | -- 如果 msg 中有 temperature 字段,表示温度值 5 | if msg.temperature then 6 | -- 将温度值从摄氏度转换为华氏度 7 | msg.temperature = msg.temperature * 1.8 + 32 8 | -- 在 metadata 中添加一个字段,表示温度单位 9 | metadata.unit = "F" 10 | end 11 | -- 返回修改后的 msg, metadata, msgType 12 | return msg, metadata, msgType 13 | end 14 | --------------------------------------------------------------------------------