├── .github ├── actions │ └── setup-db │ │ ├── Dockerfile │ │ ├── action.yml │ │ └── entrypoint.sh └── workflows │ ├── pr-receive.yml │ ├── pr-test.yml │ └── unit-test.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── api ├── api.go ├── api_test.go ├── handler.go ├── http.go ├── process.go ├── process_test.go └── types.go ├── application ├── application.go ├── application_test.go ├── disk │ ├── disk.go │ ├── disk_test.go │ └── fs.go ├── ignore │ ├── ignore.go │ ├── ignore_ported_test.go │ └── ignore_test.go ├── types.go └── yaz │ ├── build.go │ ├── build_test.go │ ├── ciphers │ ├── aes.go │ └── ase_test.go │ ├── fs.go │ ├── types.go │ ├── yaz.go │ └── yaz_test.go ├── cast ├── http.go └── http_test.go ├── connector ├── connector.go ├── connector_test.go ├── database │ └── xun.go ├── moapi │ └── moapi.go ├── mongo │ └── mongo.go ├── openai │ └── openai.go ├── redis │ └── redis.go └── types.go ├── diff ├── diff.go ├── diff_test.go └── process.go ├── dns ├── dns.go ├── dsn_test.go └── types.go ├── encoding ├── base64 │ └── base64.go ├── base64_test.go ├── encoding.go ├── hex │ └── hex.go ├── hex_test.go ├── json │ └── json.go ├── json_test.go ├── xml │ ├── decoder.go │ └── xml.go ├── yaml │ └── yaml.go └── yaml_test.go ├── flow ├── exec.go ├── exec_test.go ├── flow.go ├── flow_test.go ├── process.go ├── process_test.go └── types.go ├── fs ├── binary │ └── README.md ├── dsl │ ├── dsl.go │ └── dsl_test.go ├── fs.go ├── fs_test.go ├── mongo │ └── README.md ├── process.go ├── process_test.go ├── redis │ └── README.md ├── s3 │ └── README.md ├── system │ ├── README.md │ └── system.go ├── types.go └── xun │ └── README.md ├── go.mod ├── go.sum ├── helper ├── bind.go ├── colorjson.go ├── helper.go ├── utils.go └── utils_test.go ├── http ├── http.go ├── http_test.go ├── process.go ├── process_test.go └── types.go ├── lang ├── lang.go ├── lang_test.go └── types.go ├── model ├── README.md ├── atomic.go ├── atomic_test.go ├── column.go ├── crypt.go ├── filter.go ├── index.go ├── migrate.go ├── model.go ├── model_test.go ├── process.go ├── process_test.go ├── query.go ├── query_test.go ├── snap.go ├── snap_test.go ├── stack.go ├── types.go ├── url.go ├── url_test.go ├── validation.go └── validation_test.go ├── plan ├── README.md ├── plan.go ├── plan_test.go ├── shared.go └── types.go ├── plugin ├── plugin.go ├── plugin.types.go ├── plugin_test.go ├── process.go └── process_test.go ├── process ├── args.go ├── args_test.go ├── process.go ├── process_test.go └── types.go ├── query ├── assets │ ├── conditions │ │ ├── base.json │ │ ├── error.json │ │ └── query.json │ ├── expressions │ │ ├── alias.json │ │ ├── base.json │ │ ├── fields.json │ │ └── type.json │ ├── full.json │ ├── groups │ │ ├── error.json │ │ ├── json.json │ │ ├── mix.json │ │ ├── strict.json │ │ └── sugar.json │ ├── orders │ │ ├── error.json │ │ ├── mix.json │ │ ├── strict.json │ │ └── sugar.json │ ├── queries │ │ ├── from.json │ │ ├── groups.array.json │ │ ├── groups.json │ │ ├── havings.json │ │ ├── joins.json │ │ ├── limit.json │ │ ├── orders.json │ │ ├── select.json │ │ ├── sql.json │ │ ├── subquery.json │ │ ├── subquery.name.json │ │ ├── unions.json │ │ └── wheres.json │ └── tables │ │ ├── base.json │ │ └── error.json ├── engine.go ├── gou.go ├── gou │ ├── build.go │ ├── build_test.go │ ├── condition.go │ ├── condition_test.go │ ├── expression.go │ ├── expression_test.go │ ├── groups.go │ ├── groups_test.go │ ├── having.go │ ├── init_test.go │ ├── orders.go │ ├── orders_test.go │ ├── query.go │ ├── query_test.go │ ├── sql.go │ ├── table.go │ ├── table_test.go │ ├── types.go │ ├── validate.go │ └── where.go ├── gou_test.go ├── init_test.go ├── share │ ├── dsl.go │ └── flow.fliter.go ├── tai.go └── tai │ └── types.go ├── rag ├── README.md ├── driver │ ├── openai │ │ ├── vectorizer.go │ │ └── vectorizer_test.go │ ├── qdrant │ │ ├── README.md │ │ ├── engine.go │ │ ├── engine_test.go │ │ ├── file.go │ │ └── file_test.go │ └── types.go ├── rag.go └── rag_test.go ├── runtime ├── transform │ ├── transform.go │ └── transform_test.go └── v8 │ ├── bridge │ ├── bridge.go │ ├── bridge_test.go │ ├── function.go │ ├── go_test.go │ ├── js_test.go │ └── promise.go │ ├── context.go │ ├── dispatcher.go │ ├── functions │ ├── atob │ │ ├── atob.go │ │ └── atob_test.go │ ├── btoa │ │ ├── btoa.go │ │ └── btoa_test.go │ ├── eval │ │ ├── READEME.md │ │ ├── eval.go │ │ └── eval_test.go │ ├── lang │ │ ├── lang.go │ │ └── lang_test.go │ ├── process │ │ ├── process.go │ │ └── process_test.go │ └── studio │ │ └── studio.go │ ├── isolate.go │ ├── isolate_test.go │ ├── objects │ ├── console │ │ ├── console.go │ │ └── console_test.go │ ├── exception │ │ ├── exception.go │ │ └── exception_test.go │ ├── fs │ │ ├── fs.go │ │ └── fs_test.go │ ├── http │ │ ├── http.go │ │ └── http_test.go │ ├── job │ │ ├── job.go │ │ └── job_test.go │ ├── log │ │ ├── log.go │ │ └── log_test.go │ ├── plan │ │ ├── plan.go │ │ └── plan_test.go │ ├── query │ │ ├── query.go │ │ └── query_test.go │ ├── store │ │ ├── store.go │ │ └── store_test.go │ ├── time │ │ ├── time.go │ │ └── time_test.go │ └── websocket │ │ ├── websocket.go │ │ └── websocket_test.go │ ├── option.go │ ├── process.go │ ├── process_test.go │ ├── require.go │ ├── require_test.go │ ├── runner.go │ ├── script.go │ ├── script_test.go │ ├── sourcemap.go │ ├── sourcemap_test.go │ ├── store │ ├── cache.go │ ├── context.go │ ├── isolate.go │ ├── store.go │ └── types.go │ ├── studio_test.go │ ├── tests │ └── plan_test.go │ ├── tsconfig.go │ ├── tsconfig_test.go │ ├── types.go │ ├── v8.go │ └── v8_test.go ├── schedule ├── process.go └── process_test.go ├── schema ├── README.md ├── process.go ├── process_test.go ├── schema.go ├── types │ ├── blueprint.go │ ├── diff.go │ └── types.go ├── xun │ ├── blueprint.go │ ├── column.go │ ├── index.go │ └── xun.go └── xun_test.go ├── server ├── http │ ├── http.go │ ├── http_test.go │ └── types.go └── websocket │ ├── README.md │ ├── api.ws.go │ ├── api.ws_test.go │ ├── websocket.go │ ├── websocket.type.go │ └── websocket_test.go ├── session ├── buntdb.go ├── buntdb_test.go ├── process.go ├── process_test.go ├── redis.go ├── redis_test.go ├── session.go └── types.go ├── socket ├── README.md ├── client.go ├── client_test.go ├── server.go ├── server_test.go ├── socket.go ├── socket.type.go ├── socket_test.go └── types.go ├── ssl ├── certificate.go ├── certificate_test.go ├── process.go ├── process_test.go ├── ssl.go ├── ssl_test.go └── types.go ├── store ├── file │ └── README.md ├── lru │ ├── README.md │ ├── lru.go │ └── types.go ├── mongo │ ├── README.md │ ├── mongo.go │ └── types.go ├── process.go ├── process_test.go ├── redis │ ├── README.md │ ├── redis.go │ └── types.go ├── store.go ├── store.types.go ├── store_test.go └── types.go ├── task ├── process.go ├── process_test.go ├── task.go ├── task_test.go └── types.go ├── types ├── fs.go ├── query.types.go └── query.url.go └── websocket ├── client.go ├── client_test.go ├── hub.go ├── types.go ├── upgrader.go └── upgrader_test.go /.github/actions/setup-db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM docker:stable 2 | 3 | 4 | COPY entrypoint.sh /entrypoint.sh 5 | RUN chmod +x /entrypoint.sh 6 | ENTRYPOINT ["/entrypoint.sh"] -------------------------------------------------------------------------------- /.github/actions/setup-db/action.yml: -------------------------------------------------------------------------------- 1 | inputs: 2 | kind: 3 | description: "Chose the kind of database (MySQL8.0, MySQL5.7, Postgres9.6, Postgres14, SQLite3)" 4 | required: true 5 | db: 6 | description: "The name of database" 7 | required: false 8 | default: "github" 9 | port: 10 | description: "The port of database" 11 | required: false 12 | user: 13 | description: "The user of database" 14 | required: false 15 | default: "github" 16 | password: 17 | description: "The passowrd of database" 18 | required: false 19 | default: "123456" 20 | runs: 21 | using: "docker" 22 | image: "Dockerfile" 23 | -------------------------------------------------------------------------------- /.github/actions/setup-db/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker_run="docker run" 4 | 5 | startMySQL() { 6 | VERSION=$1 7 | PORT="3306" 8 | if [ ! -z "$INPUT_PORT" ]; then 9 | PORT=$INPUT_PORT 10 | fi 11 | 12 | echo "Start MySQL $VERSION" 13 | docker_run="$docker_run -e MYSQL_ROOT_PASSWORD=$INPUT_PASSWORD -e MYSQL_USER=$INPUT_USER -e MYSQL_PASSWORD=$INPUT_PASSWORD" 14 | docker_run="$docker_run -e MYSQL_DATABASE=$INPUT_DB" 15 | docker_run="$docker_run -d -p $PORT:3306 mysql:$VERSION --port=3306" 16 | docker_run="$docker_run --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci" 17 | sh -c "$docker_run" 18 | 19 | DB_DSN="root:$INPUT_PASSWORD@tcp(127.0.0.1:$PORT)/$INPUT_DB?charset=utf8mb4&parseTime=True&loc=Local" 20 | echo "DSN=$DB_DSN" >> $GITHUB_ENV 21 | echo "DB_DRIVER=mysql" >> $GITHUB_ENV 22 | echo $DB_DSN 23 | } 24 | 25 | startPostgres() { 26 | VERSION=$1 27 | PORT="5432" 28 | if [ ! -z "$INPUT_PORT" ]; then 29 | PORT=$INPUT_PORT 30 | fi 31 | 32 | echo "Start Postgres $VERSION" 33 | docker_run="docker run" 34 | docker_run="$docker_run -e POSTGRES_DB=$INPUT_DB" 35 | docker_run="$docker_run -e POSTGRES_USER=$INPUT_USER" 36 | docker_run="$docker_run -e POSTGRES_PASSWORD=$INPUT_PASSWORD" 37 | docker_run="$docker_run -d -p $PORT:5432 postgres:$VERSION" 38 | 39 | DB_DSN="postgres://$INPUT_USER:$INPUT_PASSWORD@127.0.0.1:$PORT/$INPUT_DB?sslmode=disable" 40 | echo "DSN=$DB_DSN" >> $GITHUB_ENV 41 | echo "DB_DRIVER=postgres" >> $GITHUB_ENV 42 | echo $DB_DSN 43 | } 44 | 45 | startSQLite3() { 46 | echo "Start SQLite3" 47 | echo "DSN=file:$INPUT_DB.db?cache=shared" >> $GITHUB_ENV 48 | echo "DB_DRIVER=sqlite3" >> $GITHUB_ENV 49 | } 50 | 51 | # MySQL8.0, MySQL5.7, Postgres9.6, Postgres14, SQLite3 52 | case $INPUT_KIND in 53 | MySQL8.0) 54 | startMySQL 8.0 55 | ;; 56 | MySQL5.7) 57 | startMySQL 5.7 58 | ;; 59 | Postgres9.6) 60 | startPostgres 9.6 61 | ;; 62 | Postgres14) 63 | startPostgres 14 64 | ;; 65 | SQLite3) 66 | startSQLite3 67 | ;; 68 | esac 69 | -------------------------------------------------------------------------------- /.github/workflows/pr-receive.yml: -------------------------------------------------------------------------------- 1 | name: Receive PR 2 | 3 | # read-only repo token 4 | # no access to secrets 5 | on: 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Save PR number 17 | run: | 18 | mkdir -p ./pr 19 | echo ${{ github.event.number }} > ./pr/NR 20 | echo ${{ github.event.pull_request.head.sha }} > ./pr/SHA 21 | - uses: actions/upload-artifact@v4 22 | with: 23 | name: pr 24 | path: pr/ 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | .DS_Store 17 | *.dev.sh 18 | app/plugins/dist 19 | app/plugins/logs 20 | app/db/*.db 21 | env.*.sh 22 | service/db/yao.db 23 | tests/yao/workshop/cache 24 | tests/yao/workshop/* 25 | .idea 26 | tests/app/db 27 | tests/app/data 28 | test.log 29 | *.prompt.md 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO ?= go 2 | GOFMT ?= gofmt "-s" 3 | PACKAGES ?= $(shell $(GO) list ./...) 4 | VETPACKAGES ?= $(shell $(GO) list ./... | grep -v /examples/) 5 | GOFILES := $(shell find . -name "*.go") 6 | 7 | # ROOT_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 8 | TESTFOLDER := $(shell $(GO) list ./... | grep -E 'api|server/http|runtime|process|widget|rag|model|plan|schema|lang|query|task|schedule|flow|session|store|fs|http|encoding|ssl|plugin|connector|wasm|websocket$|v8|application|diff' | grep -v -E 'wamr|socket') 9 | TESTTAGS ?= "" 10 | 11 | .PHONY: test 12 | test: 13 | echo "mode: count" > coverage.out 14 | for d in $(TESTFOLDER); do \ 15 | $(GO) test -tags $(TESTTAGS) -v -covermode=count -coverprofile=profile.out -coverpkg=$$(echo $$d | sed "s/\/test$$//g") $$d > tmp.out; \ 16 | cat tmp.out; \ 17 | if grep -q "^--- FAIL" tmp.out; then \ 18 | rm tmp.out; \ 19 | exit 1; \ 20 | elif grep -q "build failed" tmp.out; then \ 21 | rm tmp.out; \ 22 | exit 1; \ 23 | elif grep -q "setup failed" tmp.out; then \ 24 | rm tmp.out; \ 25 | exit 1; \ 26 | elif grep -q "runtime error" tmp.out; then \ 27 | rm tmp.out; \ 28 | exit 1; \ 29 | fi; \ 30 | if [ -f profile.out ]; then \ 31 | cat profile.out | grep -v "mode:" >> coverage.out; \ 32 | rm profile.out; \ 33 | fi; \ 34 | done 35 | 36 | .PHONY: bench 37 | bench: 38 | for d in $(TESTFOLDER); do \ 39 | $(GO) test -run Benchmark -v -bench=. -benchtime=5s -benchmem $$d; \ 40 | done 41 | 42 | .PHONY: fmt 43 | fmt: 44 | $(GOFMT) -w $(GOFILES) 45 | 46 | .PHONY: fmt-check 47 | fmt-check: 48 | @diff=$$($(GOFMT) -d $(GOFILES)); \ 49 | if [ -n "$$diff" ]; then \ 50 | echo "Please run 'make fmt' and commit the result:"; \ 51 | echo "$${diff}"; \ 52 | exit 1; \ 53 | fi; 54 | 55 | vet: 56 | $(GO) vet $(VETPACKAGES) 57 | 58 | .PHONY: lint 59 | lint: 60 | @hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 61 | $(GO) get -u golang.org/x/lint/golint; \ 62 | fi 63 | for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done; 64 | 65 | .PHONY: misspell-check 66 | misspell-check: 67 | @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 68 | $(GO) get -u github.com/client9/misspell/cmd/misspell; \ 69 | fi 70 | misspell -error $(GOFILES) 71 | 72 | .PHONY: misspell 73 | misspell: 74 | @hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \ 75 | $(GO) get -u github.com/client9/misspell/cmd/misspell; \ 76 | fi 77 | misspell -w $(GOFILES) 78 | 79 | .PHONY: tools 80 | tools: 81 | go install golang.org/x/lint/golint@latest; \ 82 | go install github.com/client9/misspell/cmd/misspell@latest; 83 | 84 | .PHONY: migrate 85 | migrate: 86 | $(GO) test -tags $(TESTTAGS) -run TestModelMigrate$ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gou Framework 2 | 3 | [![Unit-Test](https://github.com/YaoApp/gou/actions/workflows/unit-test.yml/badge.svg)](https://github.com/YaoApp/gou/actions/workflows/unit-test.yml) 4 | [![codecov](https://codecov.io/gh/YaoApp/gou/branch/main/graph/badge.svg?token=0Y9nhoBud9)](https://codecov.io/gh/YaoApp/gou) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/yaoapp/gou)](https://goreportcard.com/report/github.com/yaoapp/gou) 6 | [![Go Reference](https://pkg.go.dev/badge/github.com/yaoapp/gou.svg)](https://pkg.go.dev/github.com/yaoapp/gou) 7 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FYaoApp%2Fgou.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FYaoApp%2Fgou?ref=badge_shield) 8 | 9 | App engine framework 10 | 11 | Gou 来自易经姤卦。《象》曰: 天下有风,姤。后以施命诰四方。 12 | 13 | **Discord:** https://discord.gg/MJMQCJ2Q 14 | 15 | **Documentation:** https://yaoapps.com/en-US/doc 16 | -------------------------------------------------------------------------------- /api/process.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/yaoapp/gou/process" 4 | 5 | func init() { 6 | process.RegisterGroup("api", map[string]process.Handler{ 7 | "list": processList, 8 | }) 9 | } 10 | 11 | func processList(process *process.Process) interface{} { 12 | 13 | ids := []string{} 14 | if process.NumOfArgs() > 0 { 15 | ids = process.ArgsStrings(0) 16 | } 17 | 18 | // List all 19 | apis := map[string]*API{} 20 | if len(ids) == 0 { 21 | for id, api := range APIs { 22 | apis[id] = api 23 | } 24 | return apis 25 | } 26 | 27 | // List by ids 28 | for _, id := range ids { 29 | if api, has := APIs[id]; has { 30 | apis[id] = api 31 | } 32 | } 33 | return apis 34 | } 35 | -------------------------------------------------------------------------------- /api/process_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/yaoapp/gou/process" 8 | ) 9 | 10 | func TestProcesList(t *testing.T) { 11 | 12 | prepare(t) 13 | defer clean() 14 | 15 | var apis = process.New("api.list").Run() 16 | assert.Equal(t, 2, len(apis.(map[string]*API))) 17 | apis = process.New("api.list", []string{"user"}).Run() 18 | assert.Equal(t, 1, len(apis.(map[string]*API))) 19 | assert.Equal(t, "user", apis.(map[string]*API)["user"].ID) 20 | } 21 | -------------------------------------------------------------------------------- /api/types.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | const allowHeaders = "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, Yao-Gateway-Billing, Yao-Request-Uid, Yao-Builder-Uid" 6 | const allowMethods = "POST, GET, OPTIONS, PUT, DELETE, HEAD, PATCH" 7 | 8 | // API 数据接口 9 | type API struct { 10 | ID string `jsong:"id"` 11 | Name string 12 | File string 13 | Type string 14 | HTTP HTTP 15 | } 16 | 17 | // HTTP http 协议服务 18 | type HTTP struct { 19 | Name string `json:"name"` 20 | Version string `json:"version"` 21 | Description string `json:"description,omitempty"` 22 | Group string `json:"group,omitempty"` 23 | Guard string `json:"guard,omitempty"` 24 | Paths []Path `json:"paths,omitempty"` 25 | } 26 | 27 | // Path HTTP Path 28 | type Path struct { 29 | Label string `json:"label,omitempty"` 30 | Description string `json:"description,omitempty"` 31 | Path string `json:"path"` 32 | Method string `json:"method"` 33 | Process string `json:"process"` 34 | Guard string `json:"guard,omitempty"` 35 | In []interface{} `json:"in,omitempty"` 36 | Out Out `json:"out,omitempty"` 37 | ProcessHandler bool `json:"processHandler,omitempty"` 38 | } 39 | 40 | // Out http 输出 41 | type Out struct { 42 | Status int `json:"status"` 43 | Type string `json:"type,omitempty"` 44 | Body interface{} `json:"body,omitempty"` 45 | Headers map[string]string `json:"headers,omitempty"` 46 | Redirect *Redirect `json:"redirect,omitempty"` 47 | } 48 | 49 | // Redirect out redirect 50 | type Redirect struct { 51 | Code int `json:"code,omitempty"` 52 | Location string `json:"location,omitempty"` 53 | } 54 | 55 | type ssEventData struct { 56 | Name string 57 | Message interface{} 58 | } 59 | 60 | type argsHandler func(c *gin.Context) []interface{} 61 | -------------------------------------------------------------------------------- /application/application_test.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/yaoapp/gou/application/yaz" 9 | "github.com/yaoapp/gou/application/yaz/ciphers" 10 | "github.com/yaoapp/kun/maps" 11 | ) 12 | 13 | func TestLoad(t *testing.T) { 14 | root := os.Getenv("GOU_TEST_APPLICATION") 15 | app, err := OpenFromDisk(root) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | Load(app) 20 | assert.NotNil(t, App) 21 | } 22 | 23 | func TestOpenFromDisk(t *testing.T) { 24 | root := os.Getenv("GOU_TEST_APPLICATION") 25 | app, err := OpenFromDisk(root) 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | testParse(t, app) 30 | 31 | _, err = OpenFromDisk("/path/not-exists") 32 | assert.NotNil(t, err) 33 | } 34 | 35 | func TestOpenFromYazFile(t *testing.T) { 36 | root := os.Getenv("GOU_TEST_APPLICATION") 37 | aesCipher := ciphers.NewAES([]byte("0123456789123456")) 38 | 39 | file, err := yaz.Pack(root, aesCipher) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | defer os.Remove(file) 44 | 45 | app, err := OpenFromYazFile(file, aesCipher) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | testParse(t, app) 51 | 52 | _, err = OpenFromYazFile("/path/not-exists", nil) 53 | assert.NotNil(t, err) 54 | } 55 | 56 | func TestOpenFromYaz(t *testing.T) { 57 | root := os.Getenv("GOU_TEST_APPLICATION") 58 | aesCipher := ciphers.NewAES([]byte("0123456789123456")) 59 | 60 | file, err := yaz.Pack(root, aesCipher) 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | defer os.Remove(file) 65 | 66 | reader, err := os.Open(file) 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | defer reader.Close() 71 | 72 | app, err := OpenFromYaz(reader, file, aesCipher) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | testParse(t, app) 78 | 79 | app, err = OpenFromYazCache(file, aesCipher) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | testParse(t, app) 84 | } 85 | 86 | func testParse(t *testing.T, app Application) { 87 | 88 | data, err := app.Read("app.yao") 89 | if err != nil { 90 | t.Fatal(err) 91 | } 92 | 93 | v := maps.MapStr{} 94 | err = Parse("app.yao", data, &v) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | assert.Equal(t, "::Demo Application", v.Get("name")) 99 | 100 | // Error 101 | err = Parse("app.yao", []byte(`{"nade":"s}`), &v) 102 | assert.NotNil(t, err) 103 | 104 | // Yaml 105 | v = maps.MapStr{} 106 | err = Parse("test.yml", []byte(`name: "::Demo Application"`), &v) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | assert.Equal(t, "::Demo Application", v.Get("name")) 111 | 112 | // Error ext does not support 113 | err = Parse("app.xls", []byte(`name: "::Demo Application"`), &v) 114 | assert.NotNil(t, err) 115 | } 116 | -------------------------------------------------------------------------------- /application/disk/fs.go: -------------------------------------------------------------------------------- 1 | package disk 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // Dir is the public path 10 | type Dir string 11 | 12 | // Root return the root path 13 | func (disk *Disk) Root() string { 14 | return disk.root 15 | } 16 | 17 | // FS returns a http.FileSystem that serves files from the public directory 18 | func (disk *Disk) FS(root string) http.FileSystem { 19 | return Dir(filepath.Join(disk.root, root)) 20 | } 21 | 22 | // Open implements FileSystem using os.Open, opening files for reading rooted 23 | func (dir Dir) Open(name string) (http.File, error) { 24 | return os.Open(filepath.Join(string(dir), name)) 25 | } 26 | -------------------------------------------------------------------------------- /application/types.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import "net/http" 4 | 5 | // Application the application interface 6 | type Application interface { 7 | Walk(path string, handler func(root, filename string, isdir bool) error, patterns ...string) error 8 | Read(name string) ([]byte, error) 9 | Write(name string, content []byte) error 10 | Remove(name string) error 11 | Exists(name string) (bool, error) 12 | Watch(handler func(event string, name string), interrupt chan uint8) error 13 | Glob(pattern string) (matches []string, err error) 14 | 15 | Root() string 16 | FS(root string) http.FileSystem 17 | } 18 | 19 | // Pack the application pack 20 | type Pack struct { 21 | Name string `json:"name,omitempty"` 22 | Environments map[string]string `json:"environments,omitempty"` 23 | } 24 | -------------------------------------------------------------------------------- /application/yaz/ciphers/aes.go: -------------------------------------------------------------------------------- 1 | package ciphers 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "io" 8 | ) 9 | 10 | // AES is a type that represents the AES cipher. 11 | type AES struct { 12 | key []byte 13 | } 14 | 15 | // NewAES creates a new AES cipher. 16 | func NewAES(key []byte) AES { 17 | return AES{key: key} 18 | } 19 | 20 | // Encrypt encrypts a byte slice. 21 | func (aesCipher AES) Encrypt(reader io.Reader, writer io.Writer) error { 22 | const blockSize = aes.BlockSize 23 | key := make([]byte, blockSize) 24 | copy(key, aesCipher.key) 25 | 26 | var iv [blockSize]byte 27 | block, err := aes.NewCipher(key) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | stream := cipher.NewCFBEncrypter(block, iv[:]) 33 | buf := make([]byte, 4096) 34 | for { 35 | n, err := reader.Read(buf) 36 | if err != nil && err != io.EOF { 37 | return err 38 | } 39 | if n == 0 { 40 | break 41 | } 42 | stream.XORKeyStream(buf[:n], buf[:n]) 43 | if _, err := writer.Write(buf[:n]); err != nil { 44 | return err 45 | } 46 | } 47 | 48 | return nil 49 | } 50 | 51 | // Decrypt decrypts a byte slice. 52 | func (aesCipher AES) Decrypt(reader io.Reader, writer io.Writer) error { 53 | 54 | const blockSize = aes.BlockSize 55 | key := make([]byte, blockSize) 56 | copy(key, aesCipher.key) 57 | 58 | var iv [blockSize]byte 59 | block, err := aes.NewCipher(key) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | stream := cipher.NewCFBDecrypter(block, iv[:]) 65 | buf := make([]byte, 4096) 66 | 67 | for { 68 | n, err := reader.Read(buf) 69 | if err != nil && err != io.EOF { 70 | return err 71 | } 72 | if n == 0 { 73 | break 74 | } 75 | stream.XORKeyStream(buf[:n], buf[:n]) 76 | if _, err := writer.Write(buf[:n]); err != nil { 77 | return err 78 | } 79 | } 80 | return nil 81 | } 82 | 83 | // padding 84 | func (aesCipher AES) pkcs5Padding(ciphertext []byte, blockSize int) []byte { 85 | padding := blockSize - len(ciphertext)%blockSize 86 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 87 | return append(ciphertext, padtext...) 88 | } 89 | 90 | // unpading 91 | func (aesCipher AES) pkcs5UnPadding(origData []byte) []byte { 92 | length := len(origData) 93 | unpadding := int(origData[length-1]) 94 | return origData[:(length - unpadding)] 95 | } 96 | -------------------------------------------------------------------------------- /application/yaz/ciphers/ase_test.go: -------------------------------------------------------------------------------- 1 | package ciphers 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAES(t *testing.T) { 11 | data := []byte("hello world") 12 | aes := NewAES([]byte("0123456789123456")) 13 | reader := bytes.NewReader(data) 14 | buffer := &bytes.Buffer{} 15 | err := aes.Encrypt(reader, buffer) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | encrypted := buffer.Bytes() 20 | 21 | buffer = &bytes.Buffer{} 22 | err = aes.Decrypt(bytes.NewReader(encrypted), buffer) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | decrypted := buffer.Bytes() 27 | assert.Equal(t, data, decrypted) 28 | } 29 | -------------------------------------------------------------------------------- /application/yaz/fs.go: -------------------------------------------------------------------------------- 1 | package yaz 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // Dir is the public path 12 | type Dir struct { 13 | path string 14 | cipher Cipher 15 | } 16 | 17 | // File is the file 18 | type File struct { 19 | *os.File 20 | cipher Cipher 21 | buff []byte 22 | } 23 | 24 | // Root return the root path 25 | func (yaz *Yaz) Root() string { 26 | return yaz.root 27 | } 28 | 29 | // FS returns a http.FileSystem that serves files from the public directory 30 | func (yaz *Yaz) FS(root string) http.FileSystem { 31 | return Dir{path: filepath.Join(yaz.root, root), cipher: yaz.cipher} 32 | } 33 | 34 | // Open implements FileSystem using os.Open, opening files for reading rooted 35 | func (dir Dir) Open(name string) (http.File, error) { 36 | 37 | f, err := os.Open(filepath.Join(dir.path, name)) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | // decrypt file 43 | buff := &bytes.Buffer{} 44 | ext := strings.TrimPrefix(filepath.Ext(name), ".") 45 | if dir.cipher != nil && encryptFiles[ext] { 46 | dir.cipher.Decrypt(f, buff) 47 | } 48 | 49 | return &File{File: f, buff: buff.Bytes(), cipher: dir.cipher}, nil 50 | } 51 | 52 | // Read reads up to len(p) bytes into p. 53 | func (file *File) Read(p []byte) (n int, err error) { 54 | 55 | ext := strings.TrimPrefix(filepath.Ext(file.Name()), ".") 56 | if file.cipher != nil && encryptFiles[ext] { 57 | if len(file.buff) == 0 { 58 | return 0, nil 59 | } 60 | n = copy(p, file.buff) 61 | file.buff = file.buff[n:] 62 | return n, nil 63 | } 64 | 65 | return file.File.Read(p) 66 | } 67 | 68 | // Close closes the File, rendering it unusable for I/O. 69 | func (file *File) Close() error { 70 | file.buff = nil 71 | return file.File.Close() 72 | } 73 | -------------------------------------------------------------------------------- /application/yaz/types.go: -------------------------------------------------------------------------------- 1 | package yaz 2 | 3 | import "io" 4 | 5 | // Yaz is a type that represents a package. 6 | type Yaz struct { 7 | root string // the app root (temp dir) 8 | cipher Cipher // the cipher interface 9 | } 10 | 11 | // Cipher is a type that represents a cipher. 12 | type Cipher interface { 13 | Encrypt(reader io.Reader, writer io.Writer) error 14 | Decrypt(reader io.Reader, writer io.Writer) error 15 | } 16 | -------------------------------------------------------------------------------- /cast/http.go: -------------------------------------------------------------------------------- 1 | package cast 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | "strings" 8 | ) 9 | 10 | // AnyToHeaders cast to http.Header 11 | func AnyToHeaders(v interface{}) (http.Header, error) { 12 | values, err := AnyToURLValues(v) 13 | if err != nil { 14 | return nil, err 15 | } 16 | return http.Header(values), nil 17 | } 18 | 19 | // AnyToURLValues cast to url.Values 20 | func AnyToURLValues(v interface{}) (url.Values, error) { 21 | 22 | values := url.Values{} 23 | if v == nil { 24 | return values, nil 25 | } 26 | 27 | switch input := v.(type) { 28 | 29 | // k1=v1&k2=v2&k3=v3, ?k1=v1&k2=v2&k3=v3 30 | case string: 31 | input = strings.TrimPrefix(input, "?") 32 | query, err := url.ParseQuery(input) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return query, nil 37 | 38 | // {"k1":"v1", "k2":1, "k3":true, "k4":0.618} 39 | case map[string]interface{}: 40 | for key, val := range input { 41 | values.Add(key, fmt.Sprintf("%v", val)) 42 | } 43 | return values, nil 44 | 45 | // {"k1": "v1", "k2": "v2"}, 46 | case map[string]string: 47 | for key, val := range input { 48 | values.Add(key, val) 49 | } 50 | return values, nil 51 | 52 | // [{"k1": "v1"}, {"k1": "v11"}, {"k2": 1}, {"k3": true}, {"k4": 0.618}] 53 | case []map[string]interface{}: 54 | err := ArrayToURLValues(values, input) 55 | if err != nil { 56 | return nil, err 57 | } 58 | return values, nil 59 | 60 | // [{"k1": "v1"}, {"k1": "v11"}, {"k2": "v2"}] 61 | case []map[string]string: 62 | err := ArrayToURLValues(values, input) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return values, nil 67 | 68 | // ["k1=v1","k1"="v11","k2"="v2"] 69 | case []string: 70 | err := ArrayToURLValues(values, input) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return values, nil 75 | 76 | // ["k1=v1", "k1=v11", {"k2": 1}, {"k3": true}, "k2=v2"] 77 | case []interface{}: 78 | err := ArrayToURLValues(values, input) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return values, nil 83 | } 84 | 85 | return nil, fmt.Errorf("Unknown type %#v", v) 86 | } 87 | 88 | // ArrayToURLValues cast array to url values 89 | func ArrayToURLValues[T string | interface{} | map[string]interface{} | map[string]string](values url.Values, input []T) error { 90 | for _, item := range input { 91 | vals, err := AnyToURLValues(item) 92 | if err != nil { 93 | return err 94 | } 95 | MergeURLValues(values, vals) 96 | } 97 | return nil 98 | } 99 | 100 | // MergeURLValues merge URL Values 101 | func MergeURLValues(values, new url.Values) { 102 | for k, val := range new { 103 | for _, v := range val { 104 | values.Add(k, v) 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /connector/moapi/moapi.go: -------------------------------------------------------------------------------- 1 | package moapi 2 | 3 | import ( 4 | "github.com/yaoapp/gou/application" 5 | "github.com/yaoapp/gou/helper" 6 | "github.com/yaoapp/xun/dbal/query" 7 | "github.com/yaoapp/xun/dbal/schema" 8 | ) 9 | 10 | // Connector connector 11 | type Connector struct { 12 | id string 13 | file string 14 | Name string `json:"name"` 15 | Options Options `json:"options"` 16 | } 17 | 18 | // Options the redis connector option 19 | type Options struct { 20 | Proxy string `json:"proxy,omitempty"` 21 | Model string `json:"model,omitempty"` 22 | Key string `json:"key"` 23 | } 24 | 25 | // Register the connections from dsl 26 | func (o *Connector) Register(file string, id string, dsl []byte) error { 27 | o.id = id 28 | o.file = file 29 | err := application.Parse(file, dsl, o) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | o.Options.Proxy = helper.EnvString(o.Options.Proxy) 35 | o.Options.Model = helper.EnvString(o.Options.Model) 36 | o.Options.Key = helper.EnvString(o.Options.Key) 37 | return nil 38 | } 39 | 40 | // Is the connections from dsl 41 | func (o *Connector) Is(typ int) bool { 42 | return 6 == typ || 8 == typ 43 | } 44 | 45 | // ID get connector id 46 | func (o *Connector) ID() string { 47 | return o.id 48 | } 49 | 50 | // Query get connector query interface 51 | func (o *Connector) Query() (query.Query, error) { 52 | return nil, nil 53 | } 54 | 55 | // Schema get connector schema interface 56 | func (o *Connector) Schema() (schema.Schema, error) { 57 | return nil, nil 58 | } 59 | 60 | // Close connections 61 | func (o *Connector) Close() error { 62 | return nil 63 | } 64 | 65 | // Setting get the connection setting 66 | func (o *Connector) Setting() map[string]interface{} { 67 | 68 | host := "https://api.moapi.ai" 69 | if o.Options.Proxy != "" { 70 | host = o.Options.Proxy 71 | } 72 | 73 | return map[string]interface{}{ 74 | "host": host, 75 | "key": o.Options.Key, 76 | "model": o.Options.Model, 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /connector/openai/openai.go: -------------------------------------------------------------------------------- 1 | package openai 2 | 3 | import ( 4 | "github.com/yaoapp/gou/application" 5 | "github.com/yaoapp/gou/helper" 6 | "github.com/yaoapp/xun/dbal/query" 7 | "github.com/yaoapp/xun/dbal/schema" 8 | ) 9 | 10 | // Connector connector 11 | type Connector struct { 12 | id string 13 | file string 14 | Name string `json:"name"` 15 | Options Options `json:"options"` 16 | } 17 | 18 | // Options the redis connector option 19 | type Options struct { 20 | Proxy string `json:"proxy,omitempty"` 21 | Model string `json:"model,omitempty"` 22 | Key string `json:"key"` 23 | Azure string `json:"azure,omitempty"` // "true" or "false" 24 | } 25 | 26 | // Register the connections from dsl 27 | func (o *Connector) Register(file string, id string, dsl []byte) error { 28 | o.id = id 29 | o.file = file 30 | err := application.Parse(file, dsl, o) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | o.Options.Proxy = helper.EnvString(o.Options.Proxy) 36 | o.Options.Model = helper.EnvString(o.Options.Model) 37 | o.Options.Key = helper.EnvString(o.Options.Key) 38 | o.Options.Azure = helper.EnvString(o.Options.Azure) 39 | return nil 40 | } 41 | 42 | // Is the connections from dsl 43 | func (o *Connector) Is(typ int) bool { 44 | return 6 == typ 45 | } 46 | 47 | // ID get connector id 48 | func (o *Connector) ID() string { 49 | return o.id 50 | } 51 | 52 | // Query get connector query interface 53 | func (o *Connector) Query() (query.Query, error) { 54 | return nil, nil 55 | } 56 | 57 | // Schema get connector schema interface 58 | func (o *Connector) Schema() (schema.Schema, error) { 59 | return nil, nil 60 | } 61 | 62 | // Close connections 63 | func (o *Connector) Close() error { 64 | return nil 65 | } 66 | 67 | // Setting get the connection setting 68 | func (o *Connector) Setting() map[string]interface{} { 69 | 70 | host := "https://api.openai.com" 71 | if o.Options.Proxy != "" { 72 | host = o.Options.Proxy 73 | } 74 | 75 | return map[string]interface{}{ 76 | "host": host, 77 | "key": o.Options.Key, 78 | "model": o.Options.Model, 79 | "azure": o.Options.Azure, 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /connector/types.go: -------------------------------------------------------------------------------- 1 | package connector 2 | 3 | import ( 4 | "github.com/yaoapp/xun/dbal/query" 5 | "github.com/yaoapp/xun/dbal/schema" 6 | ) 7 | 8 | const ( 9 | // DATABASE the database connector (mysql, pgsql, oracle, sqlite ... ) 10 | DATABASE = iota + 1 11 | 12 | // REDIS the redis connector 13 | REDIS 14 | 15 | // MONGO the mongodb connector 16 | MONGO 17 | 18 | // ELASTICSEARCH the elasticsearch connector 19 | ELASTICSEARCH 20 | 21 | // KAFKA the kafka connector 22 | KAFKA 23 | 24 | // OPENAI the openai connector 25 | OPENAI 26 | 27 | // WEAVIATE the weaviate connector 28 | WEAVIATE 29 | 30 | // MOAPI the moapi connector 31 | MOAPI 32 | 33 | // SCRIPT ? the script connector ( difference with widget ?) 34 | SCRIPT 35 | ) 36 | 37 | var types = map[string]int{ 38 | "mysql": DATABASE, 39 | "sqlite": DATABASE, 40 | "sqlite3": DATABASE, 41 | "postgres": DATABASE, 42 | "oracle": DATABASE, 43 | "redis": REDIS, 44 | "mongo": MONGO, 45 | "elasticsearch": ELASTICSEARCH, 46 | "es": ELASTICSEARCH, 47 | "kafka": KAFKA, 48 | "openai": OPENAI, 49 | "weaviate": WEAVIATE, 50 | "script": SCRIPT, // ? 51 | "moapi": MOAPI, 52 | } 53 | 54 | // Connector the connector interface 55 | type Connector interface { 56 | Register(file string, id string, dsl []byte) error 57 | Query() (query.Query, error) 58 | Schema() (schema.Schema, error) 59 | Close() error 60 | ID() string 61 | Is(int) bool 62 | Setting() map[string]interface{} 63 | } 64 | 65 | // Option the option interface 66 | type Option struct { 67 | Label string `json:"label,omitempty"` 68 | Value string `json:"value,omitempty"` 69 | } 70 | 71 | // DSL the connector DSL 72 | type DSL struct { 73 | ID string `json:"-"` 74 | Type string `json:"type"` 75 | Name string `json:"name,omitempty"` 76 | Label string `json:"label,omitempty"` 77 | Version string `json:"version,omitempty"` 78 | Options map[string]interface{} `json:"options,omitempty"` 79 | } 80 | -------------------------------------------------------------------------------- /diff/process.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "github.com/yaoapp/gou/process" 5 | "github.com/yaoapp/kun/exception" 6 | ) 7 | 8 | // ProcessPatch Compare two strings and return the patch string 9 | func ProcessPatch(p *process.Process) interface{} { 10 | p.ValidateArgNums(3) 11 | text1 := p.ArgsString(0) 12 | text2 := p.ArgsString(1) 13 | checklines := p.ArgsBool(2) 14 | return PatchString(text1, text2, checklines) 15 | } 16 | 17 | // ProcessPatchApply Apply a patch string to text 18 | func ProcessPatchApply(p *process.Process) interface{} { 19 | p.ValidateArgNums(2) 20 | text := p.ArgsString(0) 21 | patch := p.ArgsString(1) 22 | applied, _, err := PatchApplyString(text, patch) 23 | if err != nil { 24 | exception.New("Patch apply error: %s", 500, err).Throw() 25 | } 26 | return applied 27 | } 28 | 29 | // ProcessReplace Replace text using a patch string 30 | func ProcessReplace(p *process.Process) interface{} { 31 | p.ValidateArgNums(2) 32 | text := p.ArgsString(0) 33 | patch := p.ArgsString(1) 34 | applied, err := Replace(text, patch) 35 | if err != nil { 36 | exception.New("Replace error: %s", 500, err).Throw() 37 | } 38 | return applied 39 | } 40 | 41 | func init() { 42 | process.Register("diff.Patch", ProcessPatch) 43 | process.Register("diff.Apply", ProcessPatchApply) 44 | process.Register("diff.Replace", ProcessReplace) 45 | } 46 | -------------------------------------------------------------------------------- /dns/dsn_test.go: -------------------------------------------------------------------------------- 1 | package dns 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "io/ioutil" 7 | "net/http" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/yaoapp/kun/utils" 13 | ) 14 | 15 | func TestLookupIP(t *testing.T) { 16 | _, err := LookupIP("github.com", true) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | res, err := LookupIP("api.mch.weixin.qq.com", true) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | assert.Contains(t, strings.Join(res, ","), ":") 27 | res, err = LookupIP("api.mch.weixin.qq.com", false) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | assert.NotContains(t, strings.Join(res, ","), ":") 32 | } 33 | 34 | func TestLookupIPCaches(t *testing.T) { 35 | ips, err := LookupIP("github.com", true) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | assert.Equal(t, ips, caches["github.com_true"]) 41 | utils.Dump(ips) 42 | 43 | _, has := caches["github.com_false"] 44 | assert.Equal(t, false, has) 45 | 46 | ips, err = LookupIP("github.com", false) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | utils.Dump(ips) 51 | assert.Equal(t, ips, caches["github.com_false"]) 52 | 53 | } 54 | 55 | func TestLookupIPHostIsIP(t *testing.T) { 56 | ips, err := LookupIP("127.0.0.1", true) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | assert.Equal(t, ips, []string{"127.0.0.1"}) 61 | } 62 | 63 | func TestDialContext(t *testing.T) { 64 | var body []byte 65 | req, err := http.NewRequest("GET", "https://api.github.com/users/yaoapp", bytes.NewBuffer(body)) 66 | tr := &http.Transport{ 67 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 68 | DialContext: DialContext(), 69 | } 70 | client := &http.Client{Transport: tr} 71 | resp, err := client.Do(req) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | defer resp.Body.Close() 76 | body, err = ioutil.ReadAll(resp.Body) // response body is []byte 77 | if err != nil { 78 | t.Fatal(err) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /dns/types.go: -------------------------------------------------------------------------------- 1 | package dns 2 | -------------------------------------------------------------------------------- /encoding/base64/base64.go: -------------------------------------------------------------------------------- 1 | package base64 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | 7 | "github.com/yaoapp/gou/process" 8 | "github.com/yaoapp/kun/exception" 9 | ) 10 | 11 | // Package base64 implements base64 encoding as specified by RFC 4648. 12 | 13 | // ProcessEncode base64 Encode 14 | func ProcessEncode(process *process.Process) interface{} { 15 | process.ValidateArgNums(1) 16 | data := []byte(fmt.Sprintf("%v", process.Args[0])) 17 | return base64.StdEncoding.EncodeToString(data) 18 | } 19 | 20 | // ProcessDecode base64 Decode 21 | func ProcessDecode(process *process.Process) interface{} { 22 | process.ValidateArgNums(1) 23 | str := process.ArgsString(0) 24 | data, err := base64.StdEncoding.DecodeString(str) 25 | if err != nil { 26 | exception.New("Base64 decode error: %s", 500, err).Throw() 27 | } 28 | return string(data) 29 | } 30 | 31 | // processDecodeBinary 32 | func processDecodeBinary(process *process.Process) interface{} { 33 | process.ValidateArgNums(1) 34 | str := process.ArgsString(0) 35 | data, err := base64.StdEncoding.DecodeString(str) 36 | if err != nil { 37 | exception.New("Base64 decode error: %s", 500, err).Throw() 38 | } 39 | return data 40 | } 41 | -------------------------------------------------------------------------------- /encoding/base64_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "encoding/base64" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/yaoapp/gou/process" 9 | ) 10 | 11 | func TestBase64Encode(t *testing.T) { 12 | data := "SomeData" 13 | res, err := process.New("encoding.base64.Encode", data).Exec() 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | v, err := base64.StdEncoding.DecodeString(res.(string)) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | assert.Equal(t, string(v), data) 22 | } 23 | 24 | func TestBase64Decode(t *testing.T) { 25 | data := base64.StdEncoding.EncodeToString([]byte("SomeData")) 26 | res, err := process.New("encoding.base64.Decode", data).Exec() 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | assert.Equal(t, "SomeData", string(res.(string))) 31 | } 32 | -------------------------------------------------------------------------------- /encoding/encoding.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "github.com/yaoapp/gou/encoding/base64" 5 | "github.com/yaoapp/gou/encoding/hex" 6 | "github.com/yaoapp/gou/encoding/json" 7 | "github.com/yaoapp/gou/encoding/xml" 8 | "github.com/yaoapp/gou/encoding/yaml" 9 | "github.com/yaoapp/gou/process" 10 | ) 11 | 12 | func init() { 13 | process.Register("encoding.base64.Encode", base64.ProcessEncode) 14 | process.Register("encoding.base64.Decode", base64.ProcessDecode) 15 | process.Register("encoding.hex.Encode", hex.ProcessEncode) 16 | process.Register("encoding.hex.Decode", hex.ProcessDecode) 17 | process.Register("encoding.json.Encode", json.ProcessEncode) 18 | process.Register("encoding.json.Decode", json.ProcessDecode) 19 | process.Register("encoding.json.Repair", json.ProcessRepair) 20 | process.Register("encoding.json.Parse", json.ProcessParse) 21 | process.Register("encoding.yaml.Encode", yaml.ProcessEncode) 22 | process.Register("encoding.yaml.Decode", yaml.ProcessDecode) 23 | process.Register("encoding.xml.Encode", xml.ProcessEncode) 24 | process.Register("encoding.xml.Decode", xml.ProcessDecode) 25 | } 26 | -------------------------------------------------------------------------------- /encoding/hex/hex.go: -------------------------------------------------------------------------------- 1 | package hex 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | 7 | "github.com/yaoapp/gou/process" 8 | "github.com/yaoapp/kun/exception" 9 | ) 10 | 11 | // ProcessEncode hex Encode 12 | func ProcessEncode(process *process.Process) interface{} { 13 | process.ValidateArgNums(1) 14 | data := []byte(fmt.Sprintf("%v", process.Args[0])) 15 | return hex.EncodeToString(data) 16 | } 17 | 18 | // ProcessDecode hex Decode 19 | func ProcessDecode(process *process.Process) interface{} { 20 | process.ValidateArgNums(1) 21 | str := process.ArgsString(0) 22 | data, err := hex.DecodeString(str) 23 | if err != nil { 24 | exception.New("Base64 decode error: %s", 500, err).Throw() 25 | } 26 | return string(data) 27 | } 28 | -------------------------------------------------------------------------------- /encoding/hex_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/yaoapp/gou/process" 9 | ) 10 | 11 | func TestHexEncode(t *testing.T) { 12 | data := "SomeData" 13 | res, err := process.New("encoding.hex.Encode", data).Exec() 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | v, err := hex.DecodeString(res.(string)) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | assert.Equal(t, string(v), data) 22 | } 23 | 24 | func TestHexDecode(t *testing.T) { 25 | data := hex.EncodeToString([]byte("SomeData")) 26 | res, err := process.New("encoding.hex.Decode", data).Exec() 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | assert.Equal(t, "SomeData", string(res.(string))) 31 | } 32 | -------------------------------------------------------------------------------- /encoding/json/json.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | jsoniter "github.com/json-iterator/go" 5 | "github.com/kaptinlin/jsonrepair" 6 | "github.com/yaoapp/gou/process" 7 | "github.com/yaoapp/kun/exception" 8 | ) 9 | 10 | // ProcessEncode json Encode 11 | func ProcessEncode(process *process.Process) interface{} { 12 | process.ValidateArgNums(1) 13 | res, err := jsoniter.Marshal(process.Args[0]) 14 | if err != nil { 15 | exception.New("JSON decode error: %s", 500, err).Throw() 16 | } 17 | return string(res) 18 | } 19 | 20 | // ProcessDecode json Decode 21 | func ProcessDecode(process *process.Process) interface{} { 22 | process.ValidateArgNums(1) 23 | data := process.ArgsString(0) 24 | var res interface{} 25 | err := jsoniter.UnmarshalFromString(data, &res) 26 | if err != nil { 27 | exception.New("JSON decode error: %s", 500, err).Throw() 28 | } 29 | return res 30 | } 31 | 32 | // ProcessRepair json Repair 33 | func ProcessRepair(process *process.Process) interface{} { 34 | process.ValidateArgNums(1) 35 | data := process.ArgsString(0) 36 | repaired, err := jsonrepair.JSONRepair(data) 37 | if err != nil { 38 | exception.New("JSON repair error: %s", 500, err).Throw() 39 | } 40 | return string(repaired) 41 | } 42 | 43 | // ProcessParse json Parse 44 | func ProcessParse(process *process.Process) interface{} { 45 | process.ValidateArgNums(1) 46 | data := process.ArgsString(0) 47 | var res interface{} 48 | err := jsoniter.UnmarshalFromString(data, &res) 49 | if err != nil { 50 | repaired, errRepair := jsonrepair.JSONRepair(data) 51 | if errRepair != nil { 52 | exception.New("JSON parse error: %s", 500, err).Throw() 53 | } 54 | 55 | // Retry 56 | errRepair = jsoniter.UnmarshalFromString(repaired, &res) 57 | if errRepair != nil { 58 | exception.New("JSON parse error: %s", 500, err).Throw() 59 | } 60 | } 61 | return res 62 | } 63 | -------------------------------------------------------------------------------- /encoding/json_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "testing" 5 | 6 | jsoniter "github.com/json-iterator/go" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/yaoapp/gou/process" 9 | ) 10 | 11 | func TestJSONEncode(t *testing.T) { 12 | data := []string{"foo", "bar"} 13 | res, err := process.New("encoding.json.Encode", data).Exec() 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | new := []string{} 19 | err = jsoniter.Unmarshal([]byte(res.(string)), &new) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | assert.Equal(t, new, data) 24 | } 25 | 26 | func TestJSONDecode(t *testing.T) { 27 | data := `["foo", "bar"]` 28 | res, err := process.New("encoding.json.Decode", data).Exec() 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | assert.Equal(t, []interface{}{"foo", "bar"}, res) 33 | } 34 | 35 | func TestJSONRepair(t *testing.T) { 36 | data := `{"name": "foo", "items": ["bar" "baz"]}` // Invalid JSON missing comma 37 | res, err := process.New("encoding.json.Repair", data).Exec() 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | // Verify the repaired JSON is valid 43 | var obj map[string]interface{} 44 | err = jsoniter.UnmarshalFromString(res.(string), &obj) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | assert.Equal(t, "foo", obj["name"]) 50 | assert.Equal(t, []interface{}{"bar", "baz"}, obj["items"]) 51 | } 52 | 53 | func TestJSONParse(t *testing.T) { 54 | // Test case 1: Valid JSON 55 | data1 := `{"name": "foo", "items": ["bar", "baz"]}` 56 | res1, err := process.New("encoding.json.Parse", data1).Exec() 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | assert.Equal(t, "foo", res1.(map[string]interface{})["name"]) 61 | assert.Equal(t, []interface{}{"bar", "baz"}, res1.(map[string]interface{})["items"]) 62 | 63 | // Test case 2: Invalid JSON that needs repair 64 | data2 := `{"name": "foo", "items": ["bar" "baz"]}` 65 | res2, err := process.New("encoding.json.Parse", data2).Exec() 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | assert.Equal(t, "foo", res2.(map[string]interface{})["name"]) 70 | assert.Equal(t, []interface{}{"bar", "baz"}, res2.(map[string]interface{})["items"]) 71 | } 72 | -------------------------------------------------------------------------------- /encoding/xml/xml.go: -------------------------------------------------------------------------------- 1 | package xml 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/yaoapp/gou/process" 9 | "github.com/yaoapp/kun/exception" 10 | ) 11 | 12 | // ProcessEncode xml Encode 13 | func ProcessEncode(process *process.Process) interface{} { 14 | process.ValidateArgNums(1) 15 | var data = process.ArgsMap(0) 16 | res, err := mapToXML(data) 17 | if err != nil { 18 | exception.New("XML decode error: %s", 500, err).Throw() 19 | } 20 | return fmt.Sprintf("\n%s\n", res) 21 | } 22 | 23 | // ProcessDecode xml Decode 24 | func ProcessDecode(process *process.Process) interface{} { 25 | process.ValidateArgNums(1) 26 | res, err := xmlToMap(process.ArgsString(0)) 27 | if err != nil { 28 | exception.New("XML decode error: %s", 500, err).Throw() 29 | } 30 | return res 31 | } 32 | 33 | func mapToXML(data map[string]interface{}) ([]byte, error) { 34 | type entry struct { 35 | XMLName xml.Name 36 | Value interface{} `xml:",chardata"` 37 | } 38 | 39 | entries := make([]entry, 0, len(data)) 40 | for key, value := range data { 41 | entries = append(entries, entry{XMLName: xml.Name{Local: key}, Value: value}) 42 | } 43 | 44 | xmlData, err := xml.MarshalIndent(entries, " ", " ") 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return xmlData, nil 50 | } 51 | 52 | func xmlToMap(data string) (map[string]interface{}, error) { 53 | decoder := NewDecoder(strings.NewReader(data)) 54 | result, err := decoder.Decode() 55 | if err != nil { 56 | return nil, err 57 | } 58 | return result, nil 59 | } 60 | -------------------------------------------------------------------------------- /encoding/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "github.com/yaoapp/gou/process" 5 | "github.com/yaoapp/kun/exception" 6 | "gopkg.in/yaml.v3" 7 | ) 8 | 9 | // ProcessEncode json Encode 10 | func ProcessEncode(process *process.Process) interface{} { 11 | process.ValidateArgNums(1) 12 | res, err := yaml.Marshal(process.Args[0]) 13 | if err != nil { 14 | exception.New("JSON decode error: %s", 500, err).Throw() 15 | } 16 | return string(res) 17 | } 18 | 19 | // ProcessDecode json Decode 20 | func ProcessDecode(process *process.Process) interface{} { 21 | process.ValidateArgNums(1) 22 | data := []byte(process.ArgsString(0)) 23 | var res interface{} 24 | err := yaml.Unmarshal(data, &res) 25 | if err != nil { 26 | exception.New("YAML decode error: %s", 500, err).Throw() 27 | } 28 | return res 29 | } 30 | -------------------------------------------------------------------------------- /encoding/yaml_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/yaoapp/gou/process" 9 | "gopkg.in/yaml.v3" 10 | ) 11 | 12 | func TestYAMLEncode(t *testing.T) { 13 | data := []string{"foo", "bar"} 14 | res, err := process.New("encoding.yaml.Encode", data).Exec() 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | new := []string{} 20 | err = yaml.Unmarshal([]byte(res.(string)), &new) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | assert.Equal(t, new, data) 25 | } 26 | 27 | func TestYAMLDecode(t *testing.T) { 28 | data := fmt.Sprintf("\n- foo\n- bar\n") 29 | res, err := process.New("encoding.yaml.Decode", data).Exec() 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | assert.Equal(t, []interface{}{"foo", "bar"}, res) 34 | } 35 | -------------------------------------------------------------------------------- /flow/exec_test.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/yaoapp/kun/any" 9 | ) 10 | 11 | func TestExec(t *testing.T) { 12 | prepare(t) 13 | flow, err := Select("basic") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | 18 | now := time.Now() 19 | yesterday := now.AddDate(0, 0, -1).Format("2006-01-02") 20 | tomorrow := now.AddDate(0, 0, 1).Format("2006-01-02") 21 | res, err := flow.Exec(yesterday, tomorrow) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | r := any.Of(res).MapStr().Dot() 27 | assert.Equal(t, yesterday, r.Get("params[0]")) 28 | assert.Equal(t, tomorrow, r.Get("params[1]")) 29 | assert.Equal(t, "U1", r.Get("data.query[0].name")) 30 | assert.Equal(t, "Duck", r.Get("data.categories[2].name")) 31 | assert.Equal(t, "U3", r.Get("data.users[1].name")) 32 | } 33 | 34 | // func TestExecQuery(t *testing.T) { 35 | // flow, _ := Select("stat") 36 | // res := maps.Of(flow.Exec("2000-01-02", "2050-12-31", 1, 2).(map[string]interface{})) 37 | // // utils.Dump(res) 38 | // assert.Equal(t, res.Dot().Get("data.manus.0.id"), int64(1)) 39 | // assert.Equal(t, res.Dot().Get("data.manus.0.short_name"), "云道天成") 40 | // assert.Equal(t, res.Dot().Get("data.manus.0.type"), "服务商") 41 | // assert.Equal(t, res.Dot().Get("data.manus.1.id"), int64(8)) 42 | // assert.Equal(t, res.Dot().Get("data.users.total"), 3) 43 | // assert.Equal(t, res.Dot().Get("data.address.city"), "丰台区") 44 | // assert.Equal(t, res.Dot().Get("params.0"), "2000-01-02") 45 | // } 46 | 47 | // func TestExecArraySet(t *testing.T) { 48 | // args := []map[string]interface{}{ 49 | // {"name": "hello", "value": "world"}, 50 | // {"name": "foo", "value": "bar"}, 51 | // } 52 | // flow, _ := Select("arrayset") 53 | // res := flow.Exec(args) 54 | // utils.Dump(res) 55 | // } 56 | 57 | // func TestExecGlobalSession(t *testing.T) { 58 | // sid := session.ID() 59 | // session.Global().ID(sid).Set("id", 1) 60 | // flow, _ := Select("user.info") 61 | // flow.WithSID(sid).WithGlobal(map[string]interface{}{"foo": "bar"}) 62 | // res := maps.Of(flow.Exec().(map[string]interface{})).Dot() 63 | // assert.Equal(t, float64(1), res.Get("ID")) 64 | // assert.Equal(t, float64(1), res.Get("会话信息.id")) 65 | // assert.Equal(t, "admin", res.Get("会话信息.type")) 66 | // assert.Equal(t, "bar", res.Get("全局信息.foo")) 67 | // assert.Equal(t, "bar", res.Get("全局信息.foo")) 68 | // assert.Equal(t, int64(1), res.Get("用户数据.id")) 69 | // assert.Equal(t, "管理员", res.Get("用户数据.name")) 70 | // assert.Equal(t, "admin", res.Get("用户数据.type")) 71 | // assert.Equal(t, "bar", res.Get("脚本数据.global.foo")) 72 | // assert.Equal(t, float64(1), res.Get("脚本数据.session.id")) 73 | // assert.Equal(t, "admin", res.Get("脚本数据.session.type")) 74 | // } 75 | -------------------------------------------------------------------------------- /flow/flow.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yaoapp/kun/log" 7 | 8 | "github.com/yaoapp/gou/application" 9 | "github.com/yaoapp/gou/query" 10 | ) 11 | 12 | // Flows 已加载工作流列表 13 | var Flows = map[string]*Flow{} 14 | 15 | // Load the flow 16 | func Load(file string, id string) (*Flow, error) { 17 | 18 | data, err := application.App.Read(file) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | flow := Flow{ID: id, File: file} 24 | err = application.Parse(file, data, &flow) 25 | if err != nil { 26 | return nil, err 27 | } 28 | 29 | flow.prepare() 30 | Flows[id] = &flow 31 | return Flows[id], nil 32 | } 33 | 34 | // Prepare 预加载 Query DSL 35 | func (flow *Flow) prepare() { 36 | 37 | for i, node := range flow.Nodes { 38 | if node.Query == nil { 39 | continue 40 | } 41 | 42 | if node.Engine == "" { 43 | log.Error("Node %s: 未指定数据查询分析引擎", node.Name) 44 | continue 45 | } 46 | 47 | if engine, has := query.Engines[node.Engine]; has { 48 | var err error 49 | flow.Nodes[i].DSL, err = engine.Load(node.Query) 50 | if err != nil { 51 | log.With(log.F{"query": node.Query}).Error("Node %s: %s 数据分析查询解析错误", node.Name, node.Engine) 52 | } 53 | continue 54 | } 55 | log.Error("Node %s: %s 数据分析引擎尚未注册", node.Name, node.Engine) 56 | } 57 | } 58 | 59 | // Reload 重新载入API 60 | func (flow *Flow) Reload() (*Flow, error) { 61 | new, err := Load(flow.File, flow.Name) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | flow = new 67 | Flows[flow.Name] = new 68 | return flow, nil 69 | } 70 | 71 | // WithSID 设定会话ID 72 | func (flow *Flow) WithSID(sid string) *Flow { 73 | flow.Sid = sid 74 | return flow 75 | } 76 | 77 | // WithGlobal 设定全局变量 78 | func (flow *Flow) WithGlobal(global map[string]interface{}) *Flow { 79 | flow.Global = global 80 | return flow 81 | } 82 | 83 | // Select 读取已加载Flow 84 | func Select(name string) (*Flow, error) { 85 | flow, has := Flows[name] 86 | if !has { 87 | return nil, fmt.Errorf("flows.%s not loaded", name) 88 | } 89 | return flow, nil 90 | } 91 | -------------------------------------------------------------------------------- /flow/process.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "github.com/yaoapp/gou/process" 5 | "github.com/yaoapp/kun/exception" 6 | ) 7 | 8 | func init() { 9 | process.Register("flows", processFlows) 10 | } 11 | 12 | // processScripts 13 | func processFlows(process *process.Process) interface{} { 14 | 15 | flow, err := Select(process.ID) 16 | if err != nil { 17 | exception.New("flows.%s not loaded", 404, process.ID).Throw() 18 | return nil 19 | } 20 | 21 | flow.WithGlobal(process.Global).WithSID(process.Sid) 22 | 23 | res, err := flow.Exec(process.Args...) 24 | if err != nil { 25 | exception.New(err.Error(), 500).Throw() 26 | } 27 | 28 | return res 29 | } 30 | -------------------------------------------------------------------------------- /flow/process_test.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/yaoapp/gou/process" 9 | "github.com/yaoapp/kun/any" 10 | ) 11 | 12 | func TestProcess(t *testing.T) { 13 | prepare(t) 14 | 15 | now := time.Now() 16 | yesterday := now.AddDate(0, 0, -1).Format("2006-01-02") 17 | tomorrow := now.AddDate(0, 0, 1).Format("2006-01-02") 18 | p, err := process.Of("flows.basic", yesterday, tomorrow) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | res, err := p.Exec() 24 | r := any.Of(res).MapStr().Dot() 25 | assert.Equal(t, yesterday, r.Get("params[0]")) 26 | assert.Equal(t, tomorrow, r.Get("params[1]")) 27 | assert.Equal(t, "U1", r.Get("data.query[0].name")) 28 | assert.Equal(t, "Duck", r.Get("data.categories[2].name")) 29 | assert.Equal(t, "U3", r.Get("data.users[1].name")) 30 | } 31 | -------------------------------------------------------------------------------- /flow/types.go: -------------------------------------------------------------------------------- 1 | package flow 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/yaoapp/gou/query/share" 7 | ) 8 | 9 | // Flow 工作流 10 | type Flow struct { 11 | ID string `json:"-"` 12 | File string `json:"-"` 13 | Name string `json:"name"` 14 | Version string `json:"version"` 15 | Description string `json:"description,omitempty"` 16 | Nodes []Node `json:"nodes,omitempty"` 17 | Output interface{} `json:"output,omitempty"` 18 | Global map[string]interface{} // 全局变量 19 | Sid string // 会话ID 20 | } 21 | 22 | // Node 工作流节点 23 | type Node struct { 24 | Name string `json:"name,omitempty"` 25 | Process string `json:"process,omitempty"` 26 | Engine string `json:"engine,omitempty"` // 数据分析引擎名称 27 | Query interface{} `json:"query,omitempty"` // 数据分析语言 Query Source 28 | DSL share.DSL `json:"-"` // 数据分析语言 Query DSL 29 | Args []interface{} `json:"args,omitempty"` 30 | Outs []interface{} `json:"outs,omitempty"` 31 | } 32 | 33 | // Context 工作流上下文 34 | type Context struct { 35 | In []interface{} 36 | Res map[string]interface{} 37 | Context *context.Context 38 | Cancel context.CancelFunc 39 | } 40 | -------------------------------------------------------------------------------- /fs/binary/README.md: -------------------------------------------------------------------------------- 1 | Bindata 2 | -------------------------------------------------------------------------------- /fs/dsl/dsl.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "path/filepath" 7 | 8 | "github.com/yaoapp/gou/fs/system" 9 | ) 10 | 11 | // File DSL File 12 | type File struct{ *system.File } 13 | 14 | // New create a new File struct 15 | func New(root ...string) *File { 16 | systemFile := system.New(root...) 17 | return &File{File: systemFile} 18 | } 19 | 20 | // WriteFile writes data to the named file, creating it if necessary. 21 | // 22 | // If the file does not exist, WriteFile creates it with permissions perm (before umask); otherwise WriteFile truncates it before writing, without changing permissions. 23 | func (f *File) WriteFile(file string, data []byte, perm uint32) (int, error) { 24 | 25 | var err error 26 | ext := filepath.Ext(file) 27 | if ext == ".json" || ext == ".yao" || ext == ".jsonc" { 28 | data, err = f.Fmt(data) 29 | if err != nil { 30 | return 0, err 31 | } 32 | } 33 | perm = 0644 34 | return f.File.WriteFile(file, data, perm) 35 | } 36 | 37 | // Allow allow rel path 38 | func (f *File) Allow(patterns ...string) *File { 39 | f.File.Allow(patterns...) 40 | return f 41 | } 42 | 43 | // Deny deny rel path 44 | func (f *File) Deny(patterns ...string) *File { 45 | f.File.Deny(patterns...) 46 | return f 47 | } 48 | 49 | // AllowAbs allow abs path 50 | func (f *File) AllowAbs(patterns ...string) *File { 51 | f.File.AllowAbs(patterns...) 52 | return f 53 | } 54 | 55 | // DenyAbs deny abs path 56 | func (f *File) DenyAbs(patterns ...string) *File { 57 | f.File.DenyAbs(patterns...) 58 | return f 59 | } 60 | 61 | // Fmt data 62 | func (f *File) Fmt(data []byte) ([]byte, error) { 63 | var out bytes.Buffer 64 | err := json.Indent(&out, data, "", " ") 65 | if err != nil { 66 | return nil, err 67 | } 68 | return out.Bytes(), nil 69 | } 70 | -------------------------------------------------------------------------------- /fs/dsl/dsl_test.go: -------------------------------------------------------------------------------- 1 | package dsl 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestDSLWriteFile(t *testing.T) { 12 | data := []byte(`{"foo": "bar", "hello":{ "int": 1, "float": 0.618}}`) 13 | shoud := []byte(`{ 14 | "foo": "bar", 15 | "hello": { 16 | "int": 1, 17 | "float": 0.618 18 | } 19 | }`) 20 | 21 | file := "test.json" 22 | fs := New(filepath.Join(os.Getenv("GOU_TEST_APP_ROOT"), "data")) 23 | size, err := fs.WriteFile(file, data, 0644) 24 | assert.Nil(t, err) 25 | assert.Equal(t, 69, size) 26 | 27 | new, err := fs.ReadFile(file) 28 | assert.Nil(t, err) 29 | assert.Equal(t, shoud, new) 30 | 31 | data = []byte(`{"foo": "bar", "hello":{ "int": 1, "float": 0.618`) 32 | size, err = fs.WriteFile(file, data, 0644) 33 | assert.NotNil(t, err) 34 | assert.Equal(t, 0, size) 35 | } 36 | -------------------------------------------------------------------------------- /fs/mongo/README.md: -------------------------------------------------------------------------------- 1 | Mongo db 2 | -------------------------------------------------------------------------------- /fs/redis/README.md: -------------------------------------------------------------------------------- 1 | Redis 2 | -------------------------------------------------------------------------------- /fs/s3/README.md: -------------------------------------------------------------------------------- 1 | S3 2 | -------------------------------------------------------------------------------- /fs/system/README.md: -------------------------------------------------------------------------------- 1 | System I/O 2 | -------------------------------------------------------------------------------- /fs/types.go: -------------------------------------------------------------------------------- 1 | package fs 2 | 3 | import ( 4 | "io" 5 | "time" 6 | ) 7 | 8 | // FileSystem the filesystem io interface 9 | type FileSystem interface { 10 | ReadFile(file string) ([]byte, error) 11 | WriteFile(file string, data []byte, perm uint32) (int, error) 12 | Write(file string, reader io.Reader, perm uint32) (int, error) 13 | 14 | AppendFile(file string, data []byte, perm uint32) (int, error) 15 | Append(file string, reader io.Reader, perm uint32) (int, error) 16 | 17 | InsertFile(file string, offset int64, data []byte, perm uint32) (int, error) 18 | Insert(file string, offset int64, reader io.Reader, perm uint32) (int, error) 19 | 20 | ReadDir(dir string, recursive bool) ([]string, error) 21 | Mkdir(dir string, perm uint32) error 22 | MkdirAll(dir string, perm uint32) error 23 | MkdirTemp(dir string, pattern string) (string, error) 24 | Glob(pattern string) ([]string, error) 25 | 26 | ReadCloser(file string) (io.ReadCloser, error) 27 | WriteCloser(file string, perm uint32) (io.WriteCloser, error) 28 | 29 | Remove(name string) error 30 | RemoveAll(name string) error 31 | 32 | Exists(name string) (bool, error) 33 | Size(name string) (int, error) 34 | Mode(name string) (uint32, error) 35 | ModTime(name string) (time.Time, error) 36 | 37 | Chmod(name string, mode uint32) error 38 | IsDir(name string) bool 39 | IsFile(name string) bool 40 | IsLink(name string) bool 41 | 42 | Move(oldpath string, newpath string) error 43 | Copy(src string, dest string) error 44 | 45 | MimeType(name string) (string, error) 46 | 47 | Root() string 48 | 49 | Walk(path string, handler func(root, filename string, isdir bool) error, patterns ...string) error 50 | 51 | List(path string, types []string, page, pageSize int, filter func(string) bool) ([]string, int, int, error) 52 | 53 | Resize(inputPath, outputPath string, width, height uint) error 54 | 55 | CleanCache() 56 | } 57 | -------------------------------------------------------------------------------- /fs/xun/README.md: -------------------------------------------------------------------------------- 1 | Save to database 2 | -------------------------------------------------------------------------------- /helper/helper.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | jsoniter "github.com/json-iterator/go" 10 | "github.com/yaoapp/kun/any" 11 | ) 12 | 13 | // 常用函数 14 | 15 | // UnmarshalFile JSON Unmarshal 16 | func UnmarshalFile(file io.Reader, v interface{}) error { 17 | content, err := ReadFile(file) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | return jsoniter.Unmarshal(content, v) 23 | } 24 | 25 | // ReadFile 读取文件内容 26 | func ReadFile(file io.Reader) ([]byte, error) { 27 | buf := new(bytes.Buffer) 28 | _, err := buf.ReadFrom(file) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return buf.Bytes(), nil 33 | } 34 | 35 | // ReadFileString 读取文件内容, 返回String 36 | func ReadFileString(file io.Reader) (string, error) { 37 | buf := new(bytes.Buffer) 38 | _, err := buf.ReadFrom(file) 39 | if err != nil { 40 | return "", err 41 | } 42 | return buf.String(), nil 43 | } 44 | 45 | // EnvString replace $ENV.xxx with the env 46 | func EnvString(key interface{}, defaults ...string) string { 47 | k, ok := key.(string) 48 | if !ok { 49 | if len(defaults) > 0 { 50 | return defaults[0] 51 | } 52 | return "" 53 | } 54 | 55 | if ok && strings.HasPrefix(k, "$ENV.") { 56 | k = strings.TrimPrefix(k, "$ENV.") 57 | v := os.Getenv(k) 58 | if v == "" && len(defaults) > 0 { 59 | return defaults[0] 60 | } 61 | return v 62 | } 63 | return key.(string) 64 | } 65 | 66 | // EnvInt replace $ENV.xxx with the env and cast to the integer 67 | func EnvInt(key interface{}, defaults ...int) int { 68 | if k, ok := key.(string); ok && strings.HasPrefix(k, "$ENV.") { 69 | k = strings.TrimPrefix(k, "$ENV.") 70 | v := os.Getenv(k) 71 | if v == "" { 72 | if len(defaults) > 0 { 73 | return defaults[0] 74 | } 75 | return 0 76 | } 77 | return any.Of(v).CInt() 78 | } 79 | 80 | v, ok := key.(int) 81 | if !ok { 82 | if len(defaults) > 0 { 83 | return defaults[0] 84 | } 85 | return any.Of(key).CInt() 86 | } 87 | return v 88 | } 89 | -------------------------------------------------------------------------------- /helper/utils_test.go: -------------------------------------------------------------------------------- 1 | package helper 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestToString(t *testing.T) { 12 | t.Run("Basic types", func(t *testing.T) { 13 | // Test integers 14 | result := ToString(42) 15 | assert.Equal(t, "42", result) 16 | 17 | // Test float 18 | result = ToString(3.14) 19 | assert.Equal(t, "3.14", result) 20 | 21 | // Test boolean 22 | result = ToString(true) 23 | assert.Equal(t, "true", result) 24 | }) 25 | 26 | t.Run("String and bytes", func(t *testing.T) { 27 | // Test string 28 | result := ToString("hello") 29 | assert.Equal(t, "hello", result) 30 | 31 | // Test []byte 32 | result = ToString([]byte("world")) 33 | assert.Equal(t, "world", result) 34 | }) 35 | 36 | t.Run("Error type", func(t *testing.T) { 37 | // Test error 38 | err := errors.New("test error") 39 | result := ToString(err) 40 | assert.Equal(t, "test error", result) 41 | }) 42 | 43 | t.Run("JSON object", func(t *testing.T) { 44 | // Test JSON object 45 | obj := map[string]interface{}{ 46 | "foo": "bar", 47 | "num": 123, 48 | "nested": map[string]interface{}{ 49 | "key": "value", 50 | }, 51 | } 52 | result := ToString(obj) 53 | assert.Contains(t, result, `"foo": "bar"`) 54 | assert.Contains(t, result, `"num": 123`) 55 | assert.Contains(t, result, `"nested": {`) 56 | assert.Contains(t, result, `"key": "value"`) 57 | }) 58 | 59 | t.Run("Multiple values", func(t *testing.T) { 60 | // Test multiple values 61 | result := ToString("first", 123, true) 62 | lines := strings.Split(result, "\n") 63 | assert.Equal(t, 3, len(lines)) 64 | assert.Equal(t, "first", lines[0]) 65 | assert.Equal(t, "123", lines[1]) 66 | assert.Equal(t, "true", lines[2]) 67 | }) 68 | 69 | t.Run("Array", func(t *testing.T) { 70 | // Test array 71 | arr := []interface{}{"one", 2, true} 72 | result := ToString(arr) 73 | assert.Contains(t, result, `"one"`) 74 | assert.Contains(t, result, `2`) 75 | assert.Contains(t, result, `true`) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /http/types.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | ) 7 | 8 | const ( 9 | 10 | // HandlerReturnOk handler return ok 11 | HandlerReturnOk = 1 12 | 13 | // HandlerReturnBreak handler return break 14 | HandlerReturnBreak = 0 15 | 16 | // HandlerReturnError handler return error 17 | HandlerReturnError = -1 18 | ) 19 | 20 | // Request HTTP Request 21 | type Request struct { 22 | url string 23 | query url.Values 24 | headers http.Header 25 | files []File 26 | fileBytes []File 27 | data interface{} 28 | } 29 | 30 | // Response HTTP Response 31 | type Response struct { 32 | Status int `json:"status"` 33 | Data interface{} `json:"data"` 34 | Headers http.Header `json:"headers"` 35 | Code int `json:"code"` 36 | Message string `json:"message"` 37 | } 38 | 39 | // File file 40 | type File struct { 41 | Name string `json:"name"` 42 | Path string `json:"path,omitempty"` 43 | Data string `json:"data,omitempty"` 44 | data []byte 45 | } 46 | -------------------------------------------------------------------------------- /lang/types.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | // Dict the language dictionary 4 | type Dict struct { 5 | Name string 6 | Global Words 7 | Widgets map[string]Words 8 | } 9 | 10 | // Words the language words 11 | type Words map[string]string 12 | 13 | // Lang the language interface 14 | type Lang interface { 15 | Lang(trans func(widgetName string, inst string, value *string) bool) 16 | } 17 | -------------------------------------------------------------------------------- /model/atomic_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/yaoapp/kun/any" 8 | "github.com/yaoapp/kun/maps" 9 | ) 10 | 11 | func TestSave(t *testing.T) { 12 | prepare(t) 13 | defer clean() 14 | pet := Select("pet") 15 | id, err := pet.Save(maps.Map{"name": "Cookie"}) 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | assert.Equal(t, 1, any.Of(id).CInt()) 20 | } 21 | -------------------------------------------------------------------------------- /model/index.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/yaoapp/xun/dbal/schema" 5 | ) 6 | 7 | // SetIndex 设置索引 8 | func (index Index) SetIndex(table schema.Blueprint) { 9 | switch index.Type { 10 | case "unique": 11 | table.AddUnique(index.Name, index.Columns...) 12 | break 13 | case "index": 14 | table.AddIndex(index.Name, index.Columns...) 15 | break 16 | case "primary": 17 | table.AddPrimary(index.Columns...) 18 | break 19 | case "fulltext": 20 | table.AddFulltext(index.Name, index.Columns...) 21 | break 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /model/url_test.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestQueryUrlValues(t *testing.T) { 11 | params := url.Values{} 12 | params.Add("select", "name,secret,status,type") 13 | params.Add("with", "mother,addresses") 14 | params.Add("mother.select", "name,mobile,type,status") 15 | params.Add("addresses.select", "province,city,location,status") 16 | params.Add("where.status.eq", "enabled") 17 | params.Add("where.secret.notnull", "") 18 | params.Add("where.resume.null", "") 19 | params.Add("where.mobile.eq", "13900002222") 20 | params.Add("orwhere.mobile.eq", "13900001111") 21 | params.Add("where.mother.friends.status.eq", "enabled") 22 | params.Add("group.types.where.type.eq", "admin") 23 | params.Add("group.types.orwhere.type.eq", "staff") 24 | params.Add("order", "id.desc,name") 25 | param := URLToQueryParam(params) 26 | assert.Equal(t, param.Select, []interface{}{"name", "secret", "status", "type"}) 27 | assert.Equal(t, len(param.Wheres), 7) 28 | assert.Equal(t, len(param.Withs), 2) 29 | assert.Equal(t, len(param.Orders), 2) 30 | } 31 | -------------------------------------------------------------------------------- /plan/types.go: -------------------------------------------------------------------------------- 1 | package plan 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Status represents the status of a plan or task 9 | type Status int 10 | 11 | // Status constants for tracking plan and task states 12 | const ( 13 | StatusCreated Status = iota 14 | StatusRunning 15 | StatusPaused 16 | StatusCompleted 17 | StatusFailed 18 | StatusDestroyed 19 | StatusUnknown 20 | ) 21 | 22 | // Signal represents control signals for tasks 23 | type Signal int 24 | 25 | // Signal constants for task control 26 | const ( 27 | SignalPause Signal = iota 28 | SignalResume 29 | SignalStop 30 | ) 31 | 32 | // Config represents plan configuration 33 | type Config struct { 34 | // StatusCheckInterval is the interval between status checks 35 | StatusCheckInterval time.Duration 36 | } 37 | 38 | // DefaultConfig returns the default configuration 39 | func DefaultConfig() *Config { 40 | return &Config{ 41 | StatusCheckInterval: 10 * time.Millisecond, 42 | } 43 | } 44 | 45 | // Option represents a function that modifies the plan configuration 46 | type Option func(*Config) 47 | 48 | // WithStatusCheckInterval sets the status check interval 49 | func WithStatusCheckInterval(d time.Duration) Option { 50 | return func(c *Config) { 51 | c.StatusCheckInterval = d 52 | } 53 | } 54 | 55 | // Task represents a unit of work in a plan 56 | type Task struct { 57 | ID string 58 | Order int 59 | Fn TaskFunc 60 | Status Status 61 | Data interface{} 62 | Context context.Context 63 | Cancel context.CancelFunc 64 | SignalChan chan Signal 65 | } 66 | 67 | // TaskFunc is the function type that represents a task's execution unit 68 | type TaskFunc func(ctx context.Context, shared SharedSpace, signals <-chan Signal) error 69 | 70 | // Plan represents a collection of tasks with shared space 71 | type Plan struct { 72 | ID string 73 | Tasks map[string]*Task 74 | SharedSpace SharedSpace 75 | Status Status 76 | Context context.Context 77 | Cancel context.CancelFunc 78 | Config *Config 79 | } 80 | 81 | // SharedSpace represents the interface for shared storage space 82 | type SharedSpace interface { 83 | // Set stores a value in the shared space 84 | Set(key string, value interface{}) error 85 | 86 | // Get retrieves a value from the shared space 87 | Get(key string) (interface{}, error) 88 | 89 | // Delete removes a value from the shared space 90 | Delete(key string) error 91 | 92 | // Clear removes all values from the shared space 93 | Clear() error 94 | 95 | // ClearNotify removes all values from the shared space and notifies subscribers 96 | ClearNotify() error 97 | 98 | // Subscribe subscribes to changes in the shared space 99 | Subscribe(key string, callback func(key string, value interface{})) error 100 | 101 | // Unsubscribe removes a subscription 102 | Unsubscribe(key string) error 103 | } 104 | -------------------------------------------------------------------------------- /plugin/plugin.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/hashicorp/go-hclog" 10 | "github.com/hashicorp/go-plugin" 11 | "github.com/yaoapp/kun/grpc" 12 | ) 13 | 14 | // Plugins 已加载插件 15 | var Plugins = map[string]*Plugin{} 16 | 17 | // Create an hclog.Logger 18 | var pluginLogger = hclog.New(&hclog.LoggerOptions{ 19 | Name: "plugin", 20 | Output: os.Stdout, 21 | Level: hclog.Error, 22 | }) 23 | 24 | // Load a plugin 25 | func Load(file string, id string) (*Plugin, error) { 26 | 27 | // 已载入,如果进程存在杀掉重载 28 | plug, has := Plugins[id] 29 | if has { 30 | if !plug.Client.Exited() { 31 | plug.Client.Kill() 32 | } 33 | } 34 | 35 | // We're a host. Start by launching the plugin process. 36 | client := plugin.NewClient(&plugin.ClientConfig{ 37 | HandshakeConfig: grpc.Handshake, 38 | Plugins: grpc.PluginMap, 39 | Cmd: exec.Command(file), 40 | AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, 41 | Logger: pluginLogger, 42 | }) 43 | 44 | // Connect via RPC 45 | rpcClient, err := client.Client() 46 | if err != nil { 47 | return nil, fmt.Errorf("%s(%s) %s", id, file, err.Error()) 48 | } 49 | 50 | // Request the plugin 51 | raw, err := rpcClient.Dispense("model") 52 | if err != nil { 53 | return nil, fmt.Errorf("%s(%s) %s", id, file, err.Error()) 54 | } 55 | 56 | mod := raw.(grpc.Model) 57 | p := &Plugin{ 58 | Client: client, 59 | Model: mod, 60 | ID: id, 61 | File: file, 62 | } 63 | 64 | Plugins[id] = p 65 | return p, nil 66 | } 67 | 68 | // KillAll kill all loaded plugins 69 | func KillAll() { 70 | for _, plug := range Plugins { 71 | if !plug.Client.Exited() { 72 | plug.Client.Kill() 73 | } 74 | } 75 | } 76 | 77 | // SetPluginLogger 设置日志 78 | func SetPluginLogger(name string, output io.Writer, level hclog.Level) { 79 | pluginLogger = hclog.New(&hclog.LoggerOptions{ 80 | Name: name, 81 | Output: output, 82 | Level: level, 83 | }) 84 | } 85 | 86 | // Select a plugin 87 | func Select(id string) (grpc.Model, error) { 88 | var err error 89 | plug, has := Plugins[id] 90 | if !has { 91 | return nil, fmt.Errorf("plugin %s not loaded", id) 92 | 93 | } 94 | 95 | // 如果进程已退出,重载 96 | if plug.Client.Exited() { 97 | plug, err = Load(plug.File, plug.ID) 98 | if err != nil { 99 | return nil, fmt.Errorf("%s %s", id, err) 100 | } 101 | } 102 | return plug.Model, nil 103 | } 104 | 105 | // Kill a plugin process 106 | func (plugin *Plugin) Kill() { 107 | plugin.Client.Kill() 108 | } 109 | -------------------------------------------------------------------------------- /plugin/plugin.types.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/hashicorp/go-plugin" 5 | "github.com/yaoapp/kun/grpc" 6 | ) 7 | 8 | // Plugin 插件 9 | type Plugin struct { 10 | Client *plugin.Client 11 | Model grpc.Model 12 | ID string 13 | File string 14 | } 15 | -------------------------------------------------------------------------------- /plugin/plugin_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestLoadPlugin(t *testing.T) { 12 | 13 | root := os.Getenv("GOU_TEST_PLUGIN") 14 | file := path.Join(root, "user.so") 15 | p, err := Load(file, "user") 16 | if err != nil { 17 | t.Fatal(err) 18 | } 19 | defer p.Kill() 20 | 21 | user, err := Select("user") 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | res, err := user.Exec("login", "13111021983", "#991832") 27 | assert.Equal(t, res.MustMap().Dot().Get("name"), "login") 28 | assert.Equal(t, res.MustMap().Dot().Get("args.0"), "13111021983") 29 | assert.Equal(t, res.MustMap().Dot().Get("args.1"), "#991832") 30 | assert.Nil(t, err) 31 | } 32 | -------------------------------------------------------------------------------- /plugin/process.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "github.com/yaoapp/gou/process" 5 | "github.com/yaoapp/kun/exception" 6 | ) 7 | 8 | func init() { 9 | process.Register("plugins", processPlugins) 10 | } 11 | 12 | // processPlugins 13 | func processPlugins(process *process.Process) interface{} { 14 | 15 | plugin, err := Select(process.ID) 16 | if err != nil { 17 | exception.New("plugins.%s not loaded", 404, process.ID).Throw() 18 | return nil 19 | } 20 | res, err := plugin.Exec(process.Method, process.Args...) 21 | if err != nil { 22 | exception.Err(err, 500).Throw() 23 | } 24 | 25 | return res.MustValue() 26 | } 27 | -------------------------------------------------------------------------------- /plugin/process_test.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/yaoapp/gou/process" 10 | "github.com/yaoapp/kun/maps" 11 | ) 12 | 13 | func TestProcessPlugins(t *testing.T) { 14 | prepare(t) 15 | defer KillAll() 16 | 17 | p, err := process.Of("plugins.user.login", "13111021983", "#991832") 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | 22 | data, err := p.Exec() 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | res, ok := data.(maps.MapStr) 28 | if !ok { 29 | t.Fatal("return data type error") 30 | } 31 | 32 | assert.Equal(t, res.Dot().Get("name"), "login") 33 | assert.Equal(t, res.Dot().Get("args.0"), "13111021983") 34 | assert.Equal(t, res.Dot().Get("args.1"), "#991832") 35 | assert.Nil(t, err) 36 | } 37 | 38 | func prepare(t *testing.T) { 39 | KillAll() 40 | root := os.Getenv("GOU_TEST_PLUGIN") 41 | file := path.Join(root, "user.so") 42 | _, err := Load(file, "user") 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /process/args_test.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import "testing" 4 | 5 | func TestValidateArgNums(t *testing.T) { 6 | } 7 | 8 | func TestNumOfArgs(t *testing.T) { 9 | } 10 | 11 | func TestNumOfArgsIs(t *testing.T) { 12 | } 13 | 14 | func TestArgsNotNull(t *testing.T) { 15 | } 16 | 17 | func TestArgsURLValue(t *testing.T) { 18 | } 19 | 20 | func TestArgsQueryParams(t *testing.T) { 21 | } 22 | 23 | func TestArgsInt(t *testing.T) { 24 | } 25 | 26 | func TestArgsUint32(t *testing.T) { 27 | } 28 | 29 | func TestArgsString(t *testing.T) { 30 | } 31 | 32 | func TestArgsMap(t *testing.T) { 33 | } 34 | 35 | func TestArgsBool(t *testing.T) { 36 | } 37 | 38 | func TestArgsRecords(t *testing.T) { 39 | } 40 | 41 | func TestArgsStrings(t *testing.T) { 42 | } 43 | 44 | func TestArgsArray(t *testing.T) { 45 | } 46 | 47 | func TestLang(t *testing.T) { 48 | } 49 | -------------------------------------------------------------------------------- /process/types.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Process the process sturct 8 | type Process struct { 9 | Name string 10 | Group string 11 | Method string 12 | Handler string 13 | ID string 14 | Args []interface{} 15 | Global map[string]interface{} // Global vars 16 | Sid string // Session ID 17 | Context context.Context // Context 18 | Runtime Runtime // Runtime 19 | _val *interface{} // Value // The result of the process 20 | } 21 | 22 | // Runtime interface 23 | type Runtime interface { 24 | Dispose() 25 | } 26 | 27 | // Handler the process handler 28 | type Handler func(process *Process) interface{} 29 | -------------------------------------------------------------------------------- /query/assets/conditions/base.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "field": "score", "value": 20, "op": "=", "comment": "分数" }, 3 | { "field": "score", "=": 20 }, 4 | { ":score": "分数", "=": 10 }, 5 | 6 | { "field": "score", "value": 20, "op": "<", "comment": "分数" }, 7 | { "field": "score", "<": 20 }, 8 | { ":score": "分数", "<": 20 }, 9 | 10 | { "field": "score", "value": 20, "op": "<=", "comment": "分数" }, 11 | { "field": "score", "<=": 20 }, 12 | { ":score": "分数", "<=": 20 }, 13 | 14 | { "field": "score", "value": 20, "op": ">", "comment": "分数" }, 15 | { "field": "score", ">": 20 }, 16 | { ":score": "分数", ">": 20 }, 17 | 18 | { "field": "score", "value": 20, "op": ">=", "comment": "分数" }, 19 | { "field": "score", ">=": 20 }, 20 | { ":score": "分数", ">=": 20 }, 21 | 22 | { "field": "name", "value": "李", "op": "match", "comment": "姓名" }, 23 | { "field": "name", "match": "李" }, 24 | { ":name": "姓名", "match": "李" }, 25 | 26 | { "field": "name", "value": "%明", "op": "like", "comment": "姓名" }, 27 | { "field": "name", "like": "%明" }, 28 | { ":name": "姓名", "like": "%明" }, 29 | 30 | { "field": "score", "value": [10, 20], "op": "in", "comment": "分数" }, 31 | { "field": "score", "in": [10, 20] }, 32 | { ":score": "分数", "in": [10, 20] }, 33 | 34 | { "field": "name", "value": ["张三", "李四"], "op": "in", "comment": "姓名" }, 35 | { "field": "name", "in": ["张三", "李四"] }, 36 | { ":name": "姓名", "in": ["张三", "李四"] }, 37 | 38 | { "field": "name", "op": "is", "value": "null", "comment": "姓名" }, 39 | { "field": "name", "is": "null" }, 40 | { ":name": "姓名", "is": "null" }, 41 | 42 | { "field": "name", "op": "is", "value": "not null", "comment": "姓名" }, 43 | { "field": "name", "is": "not null" }, 44 | { ":name": "姓名", "is": "not null" }, 45 | 46 | { "or": true, "field": "name", "op": "match", "value": "李" }, 47 | { "or": true, "field": "name", "match": "李" }, 48 | { "or :name": "或姓名", "match": "李" }, 49 | 50 | { "or": true, "field": "name", "op": "is", "value": "not null" }, 51 | { "or": true, "field": "name", "is": "not null" }, 52 | { "or :name": "或姓名", "is": "not null" }, 53 | 54 | { "or": false, "field": "name", "op": "is", "value": "not null" }, 55 | { "or": false, "field": "name", "is": "not null" }, 56 | { ":name": "姓名", "is": "not null" }, 57 | 58 | { "field": "id", "value": 20, "op": "=" }, 59 | { "field": "name", "value": "张三", "op": "=" }, 60 | { "field": "id", "value": "{20}", "op": "=" }, 61 | { "field": "name", "value": "{'张三'}", "op": "=" }, 62 | { "field": "name", "value": "{short_name}", "op": "=" }, 63 | { "or": true, "field": "name", "value": "{short_name}", "op": "=" } 64 | ] 65 | -------------------------------------------------------------------------------- /query/assets/conditions/error.json: -------------------------------------------------------------------------------- 1 | [ 2 | { ":score": "分数" }, 3 | { "=": 10 }, 4 | { ":score": "分数", "op": "gt", "value": 20 }, 5 | { ":score": "分数", "op": "=", "value": "{hello world}" }, 6 | { "field": "score hello", "op": "=", "value": "hello" }, 7 | { "field": "score hello", "op": "=", "value": "{hello world}" }, 8 | { "field": "score", "op": "=" }, 9 | { "field": "score", "op": "is", "value": "error" }, 10 | { "field": "score", "op": "is" }, 11 | { "field": "score", "op": "is", "value": "null" } 12 | ] 13 | -------------------------------------------------------------------------------- /query/assets/conditions/query.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "comment": "北京地区的厂商: 超过平均值的成绩单", 4 | "field": "score", 5 | "op": ">", 6 | "query": { 7 | "select": [":avg(score)"], 8 | "from": "user", 9 | "wheres": [{ "field": "area", "value": "北京" }] 10 | } 11 | }, 12 | { 13 | "comment": "北京地区厂商的成绩单", 14 | "field": "score_id", 15 | "op": "in", 16 | "query": { 17 | "select": ["id"], 18 | "from": "manu", 19 | "wheres": [{ "field": "area", "value": "北京" }] 20 | } 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /query/assets/expressions/alias.json: -------------------------------------------------------------------------------- 1 | ["name", "name1", "name_2", "姓名", "姓名1", "姓名_2"] 2 | -------------------------------------------------------------------------------- /query/assets/expressions/base.json: -------------------------------------------------------------------------------- 1 | [ 2 | "*", 3 | "name as n", 4 | "name as 名称", 5 | 6 | "0.618 as 黄金分割", 7 | "1 as 数字", 8 | 9 | "'0.618' as 黄金分割字符", 10 | "'1' as 字符串", 11 | 12 | "table.name as n", 13 | "table.name as 名称", 14 | 15 | "table.object$.foo as foo", 16 | 17 | "table.object$.arr[0] as arr", 18 | "table.object$.arr[*] as arr", 19 | 20 | "table.object$.arr[0].foo as arr", 21 | "table.object$.arr[*].bar as arr", 22 | 23 | "table.array[0].foo as foo", 24 | "table.array[*].foo as foo", 25 | 26 | "$model.name as n", 27 | "$model.name as 中文", 28 | 29 | "$model.object$.foo as foo", 30 | 31 | "$model.object$.arr[0] as arr", 32 | "$model.object$.arr[*] as arr", 33 | 34 | "$model.object$.arr[0].foo as arr", 35 | "$model.object$.arr[*].bar as arr", 36 | 37 | "$model.array@[0].foo as foo", 38 | "$model.array@[*].foo as foo", 39 | 40 | ":MAX(table.id)", 41 | ":MAX(table.object$.arr[*])", 42 | ":MAX($model.object$.arr[*])", 43 | ":MAX(中文)", 44 | 45 | "?:name as name", 46 | "?:姓名 as 姓名", 47 | "?:extra.score[*] as 数组", 48 | "?:extra.group[0].name as name" 49 | ] 50 | -------------------------------------------------------------------------------- /query/assets/expressions/fields.json: -------------------------------------------------------------------------------- 1 | [ 2 | "name", 3 | "name01", 4 | "name_02", 5 | 6 | "中文", 7 | "中文01", 8 | "中文_02", 9 | 10 | "object$", 11 | "object01$", 12 | "object_02$", 13 | "对象$", 14 | "对象01$", 15 | "对象_02$", 16 | 17 | "mobile*", 18 | "手机号*", 19 | 20 | "object$.foo", 21 | "object$.foo.bar", 22 | "object$.arr[0]", 23 | "object$.arr[*]", 24 | "object$.arr[0].foo", 25 | "object$.arr[*].bar", 26 | "object$.属性1.属性2", 27 | "object$.数组[0]", 28 | "object$.数组[*]", 29 | "object$.数组[0].属性", 30 | "object$.数组[*].属性", 31 | 32 | "对象$.foo", 33 | "对象$.foo.bar", 34 | "对象$.arr[0]", 35 | "对象$.arr[*]", 36 | "对象$.arr[0].foo", 37 | "对象$.arr[*].bar", 38 | "对象$.属性1.属性2", 39 | "对象$.数组[0]", 40 | "对象$.数组[*]", 41 | "对象$.数组[0].属性", 42 | "对象$.数组[*].属性", 43 | 44 | "array@", 45 | "array01@", 46 | "array_02@", 47 | "数组@", 48 | "数组01@", 49 | "数组_02@", 50 | 51 | "array[0]", 52 | "array[*]", 53 | "数组[0]", 54 | "数组[*]", 55 | 56 | "array@[0]", 57 | "array@[*]", 58 | "数组@[0]", 59 | "数组@[*]", 60 | 61 | "array[0].foo", 62 | "array[*].bar", 63 | "array[0].foo.bar", 64 | "array[*].foo.bar", 65 | 66 | "array@[0].foo", 67 | "array@[*].bar", 68 | "array@[0].foo.bar", 69 | "array@[*].foo.bar", 70 | 71 | "array[0].arr[0]", 72 | "array[*].arr[*]", 73 | 74 | "array@[0].arr[0]", 75 | "array@[*].arr[*]", 76 | 77 | "array[0].arr[0].foo", 78 | "array[*].arr[*].bar", 79 | "array[0].arr[0].foo.bar", 80 | "array[*].arr[*].foo.bar", 81 | 82 | "array@[0].arr[0].foo", 83 | "array@[*].arr[*].bar", 84 | "array@[0].arr[0].foo.bar", 85 | "array@[*].arr[*].foo.bar", 86 | 87 | "数组[0].属性", 88 | "数组[*].属性", 89 | "数组@[0].属性", 90 | "数组@[*].属性", 91 | 92 | "数组[0].属性1.属性2", 93 | "数组[*].属性1.属性2", 94 | 95 | "数组@[0].属性1.属性2", 96 | "数组@[*].属性1.属性2", 97 | 98 | "数组[0].数组[0]", 99 | "数组[0].数组[*]", 100 | 101 | "数组@[0].数组[0]", 102 | "数组@[0].数组[*]", 103 | 104 | "数组[0].数组[0].foo", 105 | "数组[*].数组[*].bar", 106 | "数组[0].数组[0].foo.bar", 107 | "数组[*].数组[*].foo.bar", 108 | 109 | "数组@[0].数组[0].foo", 110 | "数组@[*].数组[*].bar", 111 | "数组@[0].数组[0].foo.bar", 112 | "数组@[*].数组[*].foo.bar", 113 | 114 | ":MAX(分数)", 115 | ":MAX(数组@)", 116 | ":MAX(对象$)", 117 | ":MAX(对象$.分数)", 118 | ":MAX(对象$.分数[*])", 119 | ":CONCAT(对象$.分数, 'b', 1)", 120 | 121 | "?:name", 122 | "?:绑定字段", 123 | "?:name.foo", 124 | "?:name.arr[*]", 125 | "?:name.arr[0].foo", 126 | 127 | "0", 128 | "0.618", 129 | "'123456'" 130 | ] 131 | -------------------------------------------------------------------------------- /query/assets/expressions/type.json: -------------------------------------------------------------------------------- 1 | [ 2 | "area.city@(string 20)", 3 | "area.city@(string 20)", 4 | "area.city@(string)", 5 | "area.city@.code(integer)", 6 | "price[*](float 10,2)", 7 | "price[*](decimal 10,2)", 8 | "result$.kind[*]" 9 | ] 10 | -------------------------------------------------------------------------------- /query/assets/full.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": ["name", "value"], 3 | "from": "user", 4 | "wheres": [ 5 | { "field": "manu_id", "=": 1 }, 6 | { ":short_name": "简称", "match": "腾讯" } 7 | ], 8 | "orders": [{ "field": "id", "sort": "desc" }] 9 | } 10 | -------------------------------------------------------------------------------- /query/assets/groups/error.json: -------------------------------------------------------------------------------- 1 | [[{ "fields": "id" }], "id rolslup", [{ "field": "id" }, "type rolslup"]] 2 | -------------------------------------------------------------------------------- /query/assets/groups/json.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "field": "area.city@(string 20)", "rollup": "全部城市" }, 3 | "area.city@(string 20) rollup 全部城市", 4 | { "field": "area.city@.code(integer)", "rollup": "全部代码" }, 5 | "area.city@.code(integer) rollup 全部代码", 6 | { "field": "price[*](float 10,2)", "rollup": "全部定价" }, 7 | "price[*](float 10,2) rollup 全部定价", 8 | { "field": "result$.kind[*](string)", "rollup": "全部类型" }, 9 | "result$.kind[*] rollup 全部类型" 10 | ] 11 | -------------------------------------------------------------------------------- /query/assets/groups/mix.json: -------------------------------------------------------------------------------- 1 | [ 2 | "kind", 3 | "city rollup 所有城市", 4 | "@industries", 5 | "kind,city", 6 | "kind rollup 所有类型, city", 7 | "kind ROLlup 所有类型, city rollup 所有城市", 8 | ["city"], 9 | ["kind", "city"], 10 | ["kind", { "field": "city", "rollup": "所有类型", "comment": "按类型统计" }], 11 | [{ "field": "@industries", "rollup": "所有行业", "comment": "按行业统计" }] 12 | ] 13 | -------------------------------------------------------------------------------- /query/assets/groups/strict.json: -------------------------------------------------------------------------------- 1 | [ 2 | [{ "field": "kind" }], 3 | [{ "field": "city", "rollup": "所有城市" }], 4 | [{ "field": "@industries" }], 5 | [{ "field": "kind" }, { "field": "city" }], 6 | [ 7 | { "field": "kind", "rollup": "所有类型", "comment": "按类型统计" }, 8 | { "field": "city", "comment": "按城市统计" } 9 | ], 10 | [ 11 | { "field": "kind", "rollup": "所有类型", "comment": "按类型统计" }, 12 | { "field": "city", "rollup": "所有城市", "comment": "按城市统计" } 13 | ] 14 | ] 15 | -------------------------------------------------------------------------------- /query/assets/groups/sugar.json: -------------------------------------------------------------------------------- 1 | [ 2 | "kind", 3 | "city rollup 所有城市", 4 | "@industries", 5 | "kind,city", 6 | "kind rollup 所有类型, city", 7 | "kind rollup 所有类型, city rollup 所有城市" 8 | ] 9 | -------------------------------------------------------------------------------- /query/assets/orders/error.json: -------------------------------------------------------------------------------- 1 | [ 2 | [{ "sort": "asc" }], 3 | [{ "fields": "type", "sorts": "desc" }, { "sort": "asc" }], 4 | "id error", 5 | "id desc, name error " 6 | ] 7 | -------------------------------------------------------------------------------- /query/assets/orders/mix.json: -------------------------------------------------------------------------------- 1 | [ 2 | "id", 3 | "id desc", 4 | "type asc", 5 | "id , type desc", 6 | "id desc, type", 7 | "id desc , type desc", 8 | [{ "field": "type", "sort": "desc" }, { "field": "created_at" }], 9 | ["type desc", { "field": "created_at" }] 10 | ] 11 | -------------------------------------------------------------------------------- /query/assets/orders/strict.json: -------------------------------------------------------------------------------- 1 | [ 2 | [{ "field": "id" }], 3 | [{ "field": "id", "sort": "desc" }], 4 | [{ "field": "type" }, { "field": "created_at", "sort": "asc" }], 5 | [ 6 | { "field": "type", "sort": "desc" }, 7 | { "field": "created_at", "sort": "asc", "comment": "unit-test" } 8 | ], 9 | [{ "field": "type", "sort": "desc" }, { "field": "created_at" }] 10 | ] 11 | -------------------------------------------------------------------------------- /query/assets/orders/sugar.json: -------------------------------------------------------------------------------- 1 | [ 2 | "id", 3 | "id desc", 4 | "type asc", 5 | "id , type desc", 6 | "id desc, type", 7 | "id desc , type desc" 8 | ] 9 | -------------------------------------------------------------------------------- /query/assets/queries/from.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": ["*"], 3 | "from": "table as name" 4 | } 5 | -------------------------------------------------------------------------------- /query/assets/queries/groups.array.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": [ 3 | ":max(score) as 最高分", 4 | "citys@(string 50) as 城市", 5 | "industries@(string 100) as 行业", 6 | "towns[*](string 100)", 7 | "goods.sku[*].price(decimal 11,2)", 8 | "goods.sku[*].gid(integer)", 9 | "option$.ids[*](integer) as ID" 10 | ], 11 | "from": "table as name", 12 | "groups": [ 13 | "行业", 14 | "ID", 15 | "citys@ rollup 所有城市", 16 | "towns@ rollup 所有行政区", 17 | "goods.sku[*].price rollup 合计", 18 | "goods.sku[*].gid" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /query/assets/queries/groups.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": [":max(score) as 最高分", "city as 城市", "id", "kind"], 3 | "from": "table as name", 4 | "groups": "kind, city rollup 所有城市, id rollup ID" 5 | } 6 | -------------------------------------------------------------------------------- /query/assets/queries/havings.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": [":max(score) as 最高分", "city as 城市", "id", "kind"], 3 | "from": "table as name", 4 | "groups": "kind, city rollup 所有城市, id rollup ID", 5 | "havings": [ 6 | { "field": "城市", "=": "北京" }, 7 | { "or :kind": "类型", "=": "云存储" } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /query/assets/queries/joins.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": ["id", "name", "t2.name2", "t3.name3", "t4.name4"], 3 | "from": "t1", 4 | "joins": [ 5 | { 6 | "select": ["name2"], 7 | "from": "table2 as t2", 8 | "key": "t2.id", 9 | "foreign": "t1.t2_id", 10 | "left": true 11 | }, 12 | { 13 | "select": ["name3"], 14 | "from": "table3 as t3", 15 | "key": "t3.id", 16 | "foreign": "t2.t3_id", 17 | "right": true 18 | }, 19 | { 20 | "select": ["name4"], 21 | "from": "table4 as t4", 22 | "key": "t4.id", 23 | "foreign": "t2.t4_id" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /query/assets/queries/limit.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": ["*"], 3 | "from": "table as name", 4 | "limit": 20, 5 | "offset": 10 6 | } 7 | -------------------------------------------------------------------------------- /query/assets/queries/orders.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": ["*"], 3 | "from": "table as name", 4 | "orders": [ 5 | "id desc", 6 | ":MAX(id) desc", 7 | "$table.pin", 8 | "array@.id", 9 | "object$.arr[0].id" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /query/assets/queries/select.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": [ 3 | "name as n", 4 | "name as 名称", 5 | 6 | "0.618 as 黄金分割", 7 | "1 as 数字", 8 | 9 | "'0.618' as 黄金分割字符", 10 | "'1' as 字符串", 11 | 12 | "table.name as n", 13 | "table.name as 名称", 14 | 15 | "table.mobile* as mobile", 16 | "table.secret*", 17 | 18 | "table.object$.foo as foo", 19 | 20 | "table.object$.arr[0] as arr", 21 | "table.object$.arr[*] as arr", 22 | 23 | "table.object$.arr[0].foo as arr", 24 | "table.object$.arr[*].bar as arr", 25 | 26 | "table.array[*] as arr", 27 | 28 | "table.array[0].foo as foo", 29 | "table.array[*].foo as foo", 30 | 31 | "$model.name as n", 32 | "$model.name as 中文", 33 | 34 | "$model.object$.foo as foo", 35 | 36 | "$model.object$.arr[0] as arr", 37 | "$model.object$.arr[*] as arr", 38 | 39 | "$model.object$.arr[0].foo as arr", 40 | "$model.object$.arr[*].bar as arr", 41 | 42 | "$model.array@[0].foo as foo", 43 | "$model.array@[*].foo as foo", 44 | 45 | ":MAX(table.id)", 46 | ":MAX(table.object$.arr[*])", 47 | ":MAX($model.object$.arr[*])", 48 | ":MAX(中文)", 49 | 50 | "?:name as name", 51 | "?:姓名 as 姓名", 52 | "?:extra.score[*] as 数组", 53 | "?:extra.group[0].name as name" 54 | ], 55 | "from": "user as u" 56 | } 57 | -------------------------------------------------------------------------------- /query/assets/queries/sql.json: -------------------------------------------------------------------------------- 1 | { 2 | "sql": { 3 | "comment": "查询张三", 4 | "stmt": "SELECT * FROM user WHERE name = ? AND type = ?", 5 | "args": ["张三", 20] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /query/assets/queries/subquery.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": ["id", "_SUB_.name"], 3 | "query": { 4 | "select": ["id", "name"], 5 | "from": "manu" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /query/assets/queries/subquery.name.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": ["id", "厂商.name"], 3 | "query": { 4 | "name": "厂商", 5 | "select": ["id", "name"], 6 | "from": "manu" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /query/assets/queries/unions.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": ["id", "name"], 3 | "from": "t1", 4 | "unions": [ 5 | { 6 | "select": ["id", "name"], 7 | "from": "t2" 8 | }, 9 | { 10 | "select": ["id", "name"], 11 | "from": "t3" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /query/assets/queries/wheres.json: -------------------------------------------------------------------------------- 1 | { 2 | "select": ["*"], 3 | "from": "user as u", 4 | "wheres": [ 5 | { "field": "score", "<": 100 }, 6 | { "field": "score", ">": 0 }, 7 | { "field": "id", "in": [0, 100] }, 8 | { 9 | "wheres": [ 10 | { ":name": "姓名", "match": "李" }, 11 | { "or :name": "或姓名", "match": "李" } 12 | ] 13 | }, 14 | { 15 | "field": "manu_id", 16 | "op": "in", 17 | "query": { 18 | "select": ["manu_id as id"], 19 | "from": "manu", 20 | "wheres": [{ "field": "status", "=": "enabled" }], 21 | "limit": 10 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /query/assets/tables/base.json: -------------------------------------------------------------------------------- 1 | [ 2 | "name", 3 | "$user", 4 | "name as n", 5 | "$user as u", 6 | "name as bar", 7 | "$user as foo", 8 | "$xiang.user as 用户_表" 9 | ] 10 | -------------------------------------------------------------------------------- /query/assets/tables/error.json: -------------------------------------------------------------------------------- 1 | [ 2 | "name.d", 3 | "$name中文", 4 | "name as ,", 5 | "$model.user as |", 6 | "$model.user as foo bar" 7 | ] 8 | -------------------------------------------------------------------------------- /query/engine.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/yaoapp/gou/query/share" 8 | ) 9 | 10 | // Engines registered engines 11 | var Engines = map[string]share.DSL{} 12 | 13 | // Register Register a Query Engine 14 | func Register(name string, engine share.DSL) { 15 | name = strings.ToLower(name) 16 | Engines[name] = engine 17 | } 18 | 19 | // Unregister Unregister a Query Engine 20 | func Unregister(name string) { 21 | name = strings.ToLower(name) 22 | delete(Engines, name) 23 | } 24 | 25 | // Alias set the Engine alias 26 | func Alias(name, alias string) { 27 | name = strings.ToLower(name) 28 | alias = strings.ToLower(alias) 29 | if _, has := Engines[name]; has { 30 | Engines[alias] = Engines[name] 31 | } 32 | } 33 | 34 | // Select choose one engine 35 | func Select(name string) (share.DSL, error) { 36 | engine, has := Engines[name] 37 | if !has { 38 | return nil, fmt.Errorf("%s not found", name) 39 | } 40 | return engine, nil 41 | } 42 | -------------------------------------------------------------------------------- /query/gou.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/yaoapp/gou/query/gou" 7 | ) 8 | 9 | // Gou 创建 Gou Query share.DSL 10 | func Gou(input []byte) *gou.Query { 11 | return gou.Make(input) 12 | } 13 | 14 | // GouRead 创建 Gou Query share.DSL (输入接口) 15 | func GouRead(reader io.Reader) *gou.Query { 16 | return gou.Read(reader) 17 | } 18 | 19 | // GouOpen 创建 Gou Query share.DSL (文件) 20 | func GouOpen(filename string) *gou.Query { 21 | return gou.Open(filename) 22 | } 23 | -------------------------------------------------------------------------------- /query/gou/expression_test.go: -------------------------------------------------------------------------------- 1 | package gou 2 | 3 | import ( 4 | "testing" 5 | 6 | jsoniter "github.com/json-iterator/go" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNewExpressionBase(t *testing.T) { 11 | var exps []Expression 12 | bytes := ReadFile("expressions/base.json") 13 | err := jsoniter.Unmarshal(bytes, &exps) 14 | assert.Nil(t, err) 15 | // utils.Dump(exps) 16 | // 需要验证数据 17 | for _, exp := range exps { 18 | // utils.Dump(exp.ToString()) 19 | assert.True(t, len(exp.ToString()) > 0) 20 | } 21 | } 22 | 23 | func TestNewExpressionField(t *testing.T) { 24 | var exps []Expression 25 | bytes := ReadFile("expressions/fields.json") 26 | err := jsoniter.Unmarshal(bytes, &exps) 27 | assert.Nil(t, err) 28 | // 需要验证数据 29 | for _, exp := range exps { 30 | // utils.Dump(exp.ToString()) 31 | assert.True(t, len(exp.ToString()) > 0) 32 | } 33 | } 34 | 35 | func TestNewExpressionType(t *testing.T) { 36 | var exps []Expression 37 | bytes := ReadFile("expressions/type.json") 38 | err := jsoniter.Unmarshal(bytes, &exps) 39 | assert.Nil(t, err) 40 | // 需要验证数据 41 | for _, exp := range exps { 42 | // utils.Dump(exp.ToString()) 43 | assert.True(t, len(exp.ToString()) > 0) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /query/gou/init_test.go: -------------------------------------------------------------------------------- 1 | package gou 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/yaoapp/kun/exception" 13 | "github.com/yaoapp/xun/capsule" 14 | "github.com/yaoapp/xun/dbal/query" 15 | ) 16 | 17 | // TestAPIRoot 18 | var TestQueryRoot = "/data/querys" 19 | var TestDriver = "mysql" 20 | var TestDSN = "root:123456@tcp(127.0.0.1:3306)/gou?charset=utf8mb4&parseTime=True&loc=Local" 21 | var TestAESKey = "123456" 22 | 23 | func should(t assert.TestingT, actual, expected interface{}, msgAndArgs ...interface{}) bool { 24 | return assert.Equal(t, expected, actual, msgAndArgs...) 25 | } 26 | 27 | var qb query.Query 28 | 29 | // GetFileName 30 | func GetFileName(name string) string { 31 | return path.Join(TestQueryRoot, name) 32 | } 33 | 34 | // TableName 35 | func TableName(name string) string { 36 | return strings.TrimPrefix(name, "$") 37 | } 38 | 39 | // ReadFile 40 | func ReadFile(name string) []byte { 41 | fullname := path.Join(TestQueryRoot, name) 42 | 43 | file, err := os.Open(fullname) 44 | if err != nil { 45 | exception.New("读取文件失败 %s", 500, err.Error()).Throw() 46 | } 47 | defer file.Close() 48 | 49 | buf := bytes.NewBuffer(nil) 50 | _, err = io.Copy(buf, file) 51 | if err != nil { 52 | exception.New("读取数据失败 %s", 500, err.Error()).Throw() 53 | } 54 | return buf.Bytes() 55 | } 56 | 57 | func TestMain(m *testing.M) { 58 | 59 | TestQueryRoot = os.Getenv("GOU_TEST_QUERY_ROOT") 60 | TestDriver = os.Getenv("GOU_TEST_DB_DRIVER") 61 | TestDSN = os.Getenv("GOU_TEST_DSN") 62 | TestAESKey = os.Getenv("GOU_TEST_AES_KEY") 63 | 64 | // 数据库连接 65 | switch TestDriver { 66 | case "sqlite3": 67 | capsule.AddConn("primary", "sqlite3", TestDSN).SetAsGlobal() 68 | break 69 | default: 70 | capsule.AddConn("primary", "mysql", TestDSN).SetAsGlobal() 71 | break 72 | } 73 | 74 | qb = capsule.Query() 75 | 76 | // Run test suites 77 | exitVal := m.Run() 78 | 79 | // we can do clean up code here 80 | os.Exit(exitVal) 81 | 82 | } 83 | -------------------------------------------------------------------------------- /query/gou/query_test.go: -------------------------------------------------------------------------------- 1 | package gou 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGet(t *testing.T) { 8 | // rows := Gou([]byte{}).With(qb).Get() 9 | // utils.Dump(rows) 10 | } 11 | 12 | func TestFirst(t *testing.T) { 13 | // Gou([]byte{}).With(qb).First() 14 | } 15 | 16 | func TestPaginate(t *testing.T) { 17 | // Gou([]byte{}).With(qb).Paginate() 18 | } 19 | 20 | func TestRun(t *testing.T) { 21 | // Gou([]byte{}).With(qb).Run() 22 | } 23 | -------------------------------------------------------------------------------- /query/gou/table.go: -------------------------------------------------------------------------------- 1 | package gou 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/go-errors/errors" 9 | jsoniter "github.com/json-iterator/go" 10 | ) 11 | 12 | // UnmarshalJSON for json marshalJSON 13 | func (tab *Table) UnmarshalJSON(data []byte) error { 14 | var input string 15 | err := jsoniter.Unmarshal(data, &input) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | array := RegAlias.Split(input, -1) 21 | 22 | if len(array) == 1 { 23 | tab.Name = strings.TrimSpace(array[0]) 24 | } else if len(array) == 2 { 25 | tab.Name = strings.TrimSpace(array[0]) 26 | tab.Alias = strings.TrimSpace(array[1]) 27 | } else { 28 | return errors.Errorf("%s 格式错误", input) 29 | } 30 | 31 | // 数据模型 32 | if strings.HasPrefix(tab.Name, "$") { 33 | tab.Name = strings.TrimPrefix(tab.Name, "$") 34 | tab.IsModel = true 35 | } 36 | 37 | // 过滤 "`" 38 | tab.Name = strings.ReplaceAll(tab.Name, "`", "") 39 | tab.Alias = strings.ReplaceAll(tab.Alias, "`", "") 40 | 41 | return nil 42 | } 43 | 44 | // MarshalJSON for json marshalJSON 45 | func (tab Table) MarshalJSON() ([]byte, error) { 46 | return []byte(fmt.Sprintf("\"%s\"", tab.ToString())), nil 47 | } 48 | 49 | // ToString for json marshalJSON 50 | func (tab Table) ToString() string { 51 | name := tab.Name 52 | 53 | // 数据模型 54 | if tab.IsModel { 55 | name = "$" + name 56 | } 57 | 58 | if tab.Alias != "" { 59 | name = name + " as " + tab.Alias 60 | } 61 | 62 | return name 63 | } 64 | 65 | // Validate 校验表达式格式 66 | func (tab Table) Validate() error { 67 | 68 | reg := regexp.MustCompile("^[A-Za-z0-9_\u4e00-\u9fa5]+$") 69 | mreg := regexp.MustCompile("^[a-zA-Z\\.]+$") 70 | if !tab.IsModel && !reg.MatchString(tab.Name) { 71 | return errors.Errorf("数据表名称格式不正确(%s)", tab.Name) 72 | } 73 | 74 | if tab.IsModel && !mreg.MatchString(tab.Name) { 75 | return errors.Errorf("模型名称格式不正确(%s)", tab.Name) 76 | } 77 | 78 | if tab.Alias != "" && !reg.MatchString(tab.Alias) { 79 | return errors.Errorf("别名格式不正确(%s)", tab.Name) 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /query/gou_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGouOpen(t *testing.T) { 11 | assert.Panics(t, func() { 12 | GouOpen(path.Join(TestQueryRoot, "not-exists.json")) 13 | }) 14 | gou := GouOpen(path.Join(TestQueryRoot, "full.json")) 15 | 16 | assert.Equal(t, "user", gou.From.Name) 17 | assert.Len(t, gou.Orders, 1) 18 | assert.Len(t, gou.Select, 2) 19 | assert.Len(t, gou.Wheres, 2) 20 | } 21 | -------------------------------------------------------------------------------- /query/init_test.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/yaoapp/xun/capsule" 8 | "github.com/yaoapp/xun/dbal/query" 9 | ) 10 | 11 | // TestAPIRoot 12 | var TestQueryRoot = "/data/querys" 13 | var TestDriver = "mysql" 14 | var TestDSN = "root:123456@tcp(127.0.0.1:3306)/gou?charset=utf8mb4&parseTime=True&loc=Local" 15 | var TestAESKey = "123456" 16 | 17 | var qb query.Query 18 | 19 | func TestMain(m *testing.M) { 20 | 21 | TestQueryRoot = os.Getenv("GOU_TEST_QUERY_ROOT") 22 | TestDriver = os.Getenv("GOU_TEST_DB_DRIVER") 23 | TestDSN = os.Getenv("GOU_TEST_DSN") 24 | TestAESKey = os.Getenv("GOU_TEST_AES_KEY") 25 | 26 | // 数据库连接 27 | switch TestDriver { 28 | case "sqlite3": 29 | capsule.AddConn("primary", "sqlite3", TestDSN).SetAsGlobal() 30 | break 31 | default: 32 | capsule.AddConn("primary", "mysql", TestDSN).SetAsGlobal() 33 | break 34 | } 35 | 36 | qb = capsule.Query() 37 | 38 | // Run test suites 39 | exitVal := m.Run() 40 | 41 | // we can do clean up code here 42 | os.Exit(exitVal) 43 | 44 | } 45 | -------------------------------------------------------------------------------- /query/share/dsl.go: -------------------------------------------------------------------------------- 1 | package share 2 | 3 | import ( 4 | "github.com/yaoapp/kun/maps" 5 | ) 6 | 7 | // DSL QueryDSL Interface 8 | type DSL interface { 9 | Load(interface{}) (DSL, error) // 查询条件预载入 10 | Run(data maps.Map) interface{} // 执行查询根据查询条件返回结果 11 | Get(data maps.Map) []Record // 执行查询并返回数据记录集合 12 | Paginate(data maps.Map) Paginate // 执行查询并返回带分页信息的数据记录数组 13 | First(data maps.Map) Record // 执行查询并返回一条数据记录 14 | } 15 | 16 | // Record 数据记录 17 | type Record maps.MapStrAny 18 | 19 | // Paginate 带分页信息的数据记录数组 20 | type Paginate struct { 21 | Items []Record `json:"items"` // 数据记录集合 22 | Total int `json:"total"` // 总记录数 23 | Next int `json:"next"` // 下一页,如没有下一页返回 -1 24 | Prev int `json:"prev"` // 上一页,如没有上一页返回 -1 25 | Page int `json:"page"` // 当前页码 26 | PageSize int `json:"pagesize"` // 每页记录数量 27 | PageCount int `json:"pagecnt"` // 总页数 28 | } 29 | -------------------------------------------------------------------------------- /query/share/flow.fliter.go: -------------------------------------------------------------------------------- 1 | package share 2 | 3 | // Helper 转换器 4 | type Helper func(...interface{}) interface{} 5 | 6 | // Filters 处理函数 7 | var Filters map[string]Helper = map[string]Helper{ 8 | "pluck": func(args ...interface{}) interface{} { return args }, 9 | } 10 | 11 | // RegisterHelper 注册 helper 12 | func RegisterHelper(name string, helper Helper) { 13 | Filters[name] = helper 14 | } 15 | -------------------------------------------------------------------------------- /query/tai.go: -------------------------------------------------------------------------------- 1 | package query 2 | 3 | import ( 4 | "github.com/yaoapp/gou/query/share" 5 | "github.com/yaoapp/gou/query/tai" 6 | ) 7 | 8 | // Tai Query DSL 9 | type Tai tai.QueryDSL 10 | 11 | // Run 执行查询根据查询条件返回结果 12 | func (tai Tai) Run() interface{} { 13 | return []share.Record{} 14 | } 15 | 16 | // Get 执行查询并返回数据记录集合 17 | func (tai Tai) Get() []share.Record { 18 | return []share.Record{} 19 | } 20 | 21 | // Paginate 执行查询并返回带分页信息的数据记录数组 22 | func (tai Tai) Paginate() share.Paginate { 23 | return share.Paginate{} 24 | } 25 | 26 | // First 执行查询并返回一条数据记录 27 | func (tai Tai) First() share.Record { 28 | return share.Record{} 29 | } 30 | -------------------------------------------------------------------------------- /query/tai/types.go: -------------------------------------------------------------------------------- 1 | package tai 2 | 3 | // QueryDSL Tai Query Domain Specific Language 4 | type QueryDSL struct{} 5 | -------------------------------------------------------------------------------- /rag/rag.go: -------------------------------------------------------------------------------- 1 | package rag 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/yaoapp/gou/rag/driver" 8 | "github.com/yaoapp/gou/rag/driver/openai" 9 | "github.com/yaoapp/gou/rag/driver/qdrant" 10 | ) 11 | 12 | const ( 13 | // DriverQdrant is the Qdrant vector store driver 14 | DriverQdrant = "qdrant" 15 | // DriverOpenAI is the OpenAI embeddings driver 16 | DriverOpenAI = "openai" 17 | ) 18 | 19 | // NewEngine creates a new RAG engine instance 20 | func NewEngine(driverName string, config driver.IndexConfig, vectorizer driver.Vectorizer) (driver.Engine, error) { 21 | switch driverName { 22 | case DriverQdrant: 23 | // Convert IndexConfig to qdrant.Config 24 | qConfig := qdrant.Config{ 25 | Host: config.Options["host"], 26 | APIKey: config.Options["api_key"], 27 | Vectorizer: vectorizer, 28 | } 29 | if portStr, ok := config.Options["port"]; ok { 30 | if port, err := strconv.ParseUint(portStr, 10, 32); err == nil { 31 | qConfig.Port = uint32(port) 32 | } 33 | } 34 | return qdrant.NewEngine(qConfig) 35 | default: 36 | return nil, fmt.Errorf("unsupported engine driver: %s", driverName) 37 | } 38 | } 39 | 40 | // NewVectorizer creates a new vectorizer instance 41 | func NewVectorizer(driverName string, config driver.VectorizeConfig) (driver.Vectorizer, error) { 42 | switch driverName { 43 | case DriverOpenAI: 44 | return openai.New(openai.Config{ 45 | APIKey: config.Options["api_key"], 46 | Model: config.Model, 47 | }) 48 | default: 49 | return nil, fmt.Errorf("unsupported vectorizer driver: %s", driverName) 50 | } 51 | } 52 | 53 | // NewFileUpload creates a new file upload instance 54 | func NewFileUpload(driverName string, engine driver.Engine, vectorizer driver.Vectorizer) (driver.FileUpload, error) { 55 | switch driverName { 56 | case DriverQdrant: 57 | if qEngine, ok := engine.(*qdrant.Engine); ok { 58 | return qdrant.NewFileUpload(qEngine, vectorizer) 59 | } 60 | return nil, fmt.Errorf("engine type mismatch: expected *qdrant.Engine") 61 | default: 62 | return nil, fmt.Errorf("unsupported file upload driver: %s", driverName) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /runtime/v8/bridge/bridge_test.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/yaoapp/gou/application" 9 | "rogchap.com/v8go" 10 | ) 11 | 12 | func call(ctx *v8go.Context, method string, args ...interface{}) (interface{}, error) { 13 | 14 | global := ctx.Global() 15 | jsArgs, err := JsValues(ctx, args) 16 | if err != nil { 17 | return nil, err 18 | } 19 | defer FreeJsValues(jsArgs) 20 | 21 | jsRes, err := global.MethodCall(method, Valuers(jsArgs)...) 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | goRes, err := GoValue(jsRes, ctx) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return goRes, nil 32 | } 33 | 34 | func prepare(t *testing.T) *v8go.Context { 35 | 36 | root := os.Getenv("GOU_TEST_APPLICATION") 37 | 38 | // Load app 39 | app, err := application.OpenFromDisk(root) 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | application.Load(app) 44 | 45 | file := filepath.Join("scripts", "runtime", "bridge.js") 46 | source, err := app.Read(file) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | iso := v8go.NewIsolate() 52 | ctx := v8go.NewContext(iso) 53 | _, err = ctx.RunScript(string(source), file) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | return ctx 59 | } 60 | 61 | func close(ctx *v8go.Context) { 62 | ctx.Close() 63 | ctx.Isolate().Dispose() 64 | } 65 | -------------------------------------------------------------------------------- /runtime/v8/bridge/function.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | import "fmt" 4 | 5 | // Call jsValue Function 6 | func (f FunctionT) Call(args ...interface{}) (interface{}, error) { 7 | 8 | if f.ctx == nil { 9 | return nil, fmt.Errorf("invalid context") 10 | } 11 | 12 | cb, err := f.value.AsFunction() 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | jsArgs, err := JsValues(f.ctx, args) 18 | if err != nil { 19 | return nil, err 20 | } 21 | defer FreeJsValues(jsArgs) 22 | 23 | value, err := cb.Call(f.ctx.Global(), Valuers(jsArgs)...) 24 | if err != nil { 25 | return nil, err 26 | } 27 | defer func() { 28 | if value != nil { 29 | value.Release() 30 | } 31 | }() 32 | 33 | goValue, err := GoValue(value, f.ctx) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return goValue, nil 39 | } 40 | -------------------------------------------------------------------------------- /runtime/v8/bridge/promise.go: -------------------------------------------------------------------------------- 1 | package bridge 2 | 3 | // Result returns the result of the promise. 4 | func (p PromiseT) Result() (interface{}, error) { 5 | promise, err := p.value.AsPromise() 6 | if err != nil { 7 | return nil, err 8 | } 9 | 10 | value := promise.Result() 11 | defer func() { 12 | if value != nil { 13 | value.Release() 14 | } 15 | }() 16 | 17 | goValue, err := GoValue(value, p.ctx) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | return goValue, nil 23 | } 24 | -------------------------------------------------------------------------------- /runtime/v8/functions/atob/atob.go: -------------------------------------------------------------------------------- 1 | package atob 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "github.com/yaoapp/gou/runtime/v8/bridge" 7 | "rogchap.com/v8go" 8 | ) 9 | 10 | // ExportFunction function template 11 | func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate { 12 | return v8go.NewFunctionTemplate(iso, exec) 13 | } 14 | 15 | // exec 16 | func exec(info *v8go.FunctionCallbackInfo) *v8go.Value { 17 | 18 | jsArgs := info.Args() 19 | if len(jsArgs) < 1 { 20 | return bridge.JsException(info.Context(), "missing parameters") 21 | } 22 | 23 | if !jsArgs[0].IsString() { 24 | return bridge.JsException(info.Context(), "the first parameter should be a string") 25 | } 26 | 27 | goRes, err := base64.StdEncoding.DecodeString(jsArgs[0].String()) 28 | if err != nil { 29 | return bridge.JsException(info.Context(), err.Error()) 30 | } 31 | 32 | jsRes, err := bridge.JsValue(info.Context(), string(goRes)) 33 | if err != nil { 34 | return bridge.JsException(info.Context(), err) 35 | } 36 | 37 | return jsRes 38 | } 39 | -------------------------------------------------------------------------------- /runtime/v8/functions/atob/atob_test.go: -------------------------------------------------------------------------------- 1 | package atob 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/yaoapp/gou/runtime/v8/bridge" 8 | "rogchap.com/v8go" 9 | ) 10 | 11 | func TestAtob(t *testing.T) { 12 | 13 | ctx := prepare(t, false, "", nil) 14 | defer close(ctx) 15 | 16 | jsRes, err := ctx.RunScript(` 17 | const test = () => { 18 | const result = atob("ZGVtbzoxMjM0NTY="); 19 | return { 20 | result: result, 21 | __yao_global:__yao_global, 22 | __yao_sid: __yao_sid, 23 | __YAO_SU_ROOT: __YAO_SU_ROOT, 24 | } 25 | } 26 | test() 27 | `, "") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | goRes, err := bridge.GoValue(jsRes, ctx) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | res, ok := goRes.(map[string]interface{}) 38 | if !ok { 39 | t.Fatal("result error") 40 | } 41 | 42 | assert.Equal(t, "demo:123456", res["result"]) 43 | assert.Equal(t, nil, res["__yao_global"]) 44 | assert.Equal(t, false, res["__YAO_SU_ROOT"]) 45 | } 46 | 47 | func close(ctx *v8go.Context) { 48 | ctx.Isolate().Dispose() 49 | } 50 | 51 | func prepare(t *testing.T, root bool, sid string, global map[string]interface{}) *v8go.Context { 52 | 53 | iso := v8go.NewIsolate() 54 | 55 | template := v8go.NewObjectTemplate(iso) 56 | template.Set("atob", ExportFunction(iso)) 57 | 58 | ctx := v8go.NewContext(iso, template) 59 | 60 | var err error 61 | jsGlobal := v8go.Undefined(ctx.Isolate()) 62 | jsGlobal, err = bridge.JsValue(ctx, global) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | if err = ctx.Global().Set("__YAO_SU_ROOT", root); err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | if err = ctx.Global().Set("__yao_global", jsGlobal); err != nil { 72 | t.Fatal(err) 73 | } 74 | if err = ctx.Global().Set("__yao_sid", sid); err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | return ctx 79 | } 80 | -------------------------------------------------------------------------------- /runtime/v8/functions/btoa/btoa.go: -------------------------------------------------------------------------------- 1 | package btoa 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "github.com/yaoapp/gou/runtime/v8/bridge" 7 | "rogchap.com/v8go" 8 | ) 9 | 10 | // ExportFunction function template 11 | func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate { 12 | return v8go.NewFunctionTemplate(iso, exec) 13 | } 14 | 15 | // exec 16 | func exec(info *v8go.FunctionCallbackInfo) *v8go.Value { 17 | 18 | jsArgs := info.Args() 19 | if len(jsArgs) < 1 { 20 | return bridge.JsException(info.Context(), "missing parameters") 21 | } 22 | 23 | if !jsArgs[0].IsString() { 24 | return bridge.JsException(info.Context(), "the first parameter should be a string") 25 | } 26 | 27 | goRes := base64.StdEncoding.EncodeToString([]byte(jsArgs[0].String())) 28 | jsRes, err := bridge.JsValue(info.Context(), goRes) 29 | if err != nil { 30 | return bridge.JsException(info.Context(), err) 31 | } 32 | 33 | return jsRes 34 | } 35 | -------------------------------------------------------------------------------- /runtime/v8/functions/btoa/btoa_test.go: -------------------------------------------------------------------------------- 1 | package btoa 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/yaoapp/gou/runtime/v8/bridge" 8 | "rogchap.com/v8go" 9 | ) 10 | 11 | func TestBtoa(t *testing.T) { 12 | 13 | ctx := prepare(t, false, "", nil) 14 | defer close(ctx) 15 | 16 | jsRes, err := ctx.RunScript(` 17 | const test = () => { 18 | const result = btoa("demo:123456"); 19 | return { 20 | result: result, 21 | __yao_global:__yao_global, 22 | __yao_sid: __yao_sid, 23 | __YAO_SU_ROOT: __YAO_SU_ROOT, 24 | } 25 | } 26 | test() 27 | `, "") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | goRes, err := bridge.GoValue(jsRes, ctx) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | res, ok := goRes.(map[string]interface{}) 38 | if !ok { 39 | t.Fatal("result error") 40 | } 41 | 42 | assert.Equal(t, "ZGVtbzoxMjM0NTY=", res["result"]) 43 | assert.Equal(t, nil, res["__yao_global"]) 44 | assert.Equal(t, false, res["__YAO_SU_ROOT"]) 45 | } 46 | 47 | func close(ctx *v8go.Context) { 48 | ctx.Isolate().Dispose() 49 | } 50 | 51 | func prepare(t *testing.T, root bool, sid string, global map[string]interface{}) *v8go.Context { 52 | 53 | iso := v8go.NewIsolate() 54 | 55 | template := v8go.NewObjectTemplate(iso) 56 | template.Set("btoa", ExportFunction(iso)) 57 | 58 | ctx := v8go.NewContext(iso, template) 59 | 60 | var err error 61 | jsGlobal := v8go.Undefined(ctx.Isolate()) 62 | jsGlobal, err = bridge.JsValue(ctx, global) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | if err = ctx.Global().Set("__YAO_SU_ROOT", root); err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | if err = ctx.Global().Set("__yao_global", jsGlobal); err != nil { 72 | t.Fatal(err) 73 | } 74 | if err = ctx.Global().Set("__yao_sid", sid); err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | return ctx 79 | } 80 | -------------------------------------------------------------------------------- /runtime/v8/functions/eval/READEME.md: -------------------------------------------------------------------------------- 1 | # Eval Function 2 | 3 | The `Eval` function allows JavaScript code execution from strings within the V8 runtime environment. This is useful for dynamically generating and executing code at runtime. 4 | 5 | ## Usage 6 | 7 | ```javascript 8 | // Syntax 9 | Eval(codeString, ...args); 10 | ``` 11 | 12 | ### Parameters 13 | 14 | - `codeString` (string): A JavaScript code string to evaluate. It should contain a function declaration. 15 | - `...args`: Optional parameters to pass to the evaluated function. 16 | 17 | ### Return Value 18 | 19 | Returns the result of executing the evaluated function. 20 | 21 | ## Examples 22 | 23 | ### Basic Example 24 | 25 | ```javascript 26 | // Evaluate a simple addition function 27 | const sum = Eval("function add(a, b) { return a + b; }", 5, 3); 28 | console.log(sum); // Output: 8 29 | ``` 30 | 31 | ### Accessing Context Data 32 | 33 | When running in the Yao runtime environment, the evaluated code can access the context data: 34 | 35 | ```javascript 36 | // Assume __yao_data.DATA.user = "World" 37 | const greeting = Eval( 38 | "function greet(prefix) { return prefix + ', ' + __yao_data.DATA.user + '!'; }", 39 | "Hello" 40 | ); 41 | console.log(greeting); // Output: "Hello, World!" 42 | ``` 43 | 44 | ## Implementation Details 45 | 46 | The Eval function: 47 | 48 | 1. Takes a JavaScript code string, typically containing a function definition 49 | 2. Converts the function definition to an arrow function format for execution 50 | 3. Compiles and runs the script in the V8 context 51 | 4. Passes any additional arguments to the evaluated function 52 | 5. Returns the result of the function execution 53 | 54 | ## Notes 55 | 56 | - The evaluated code has access to the current V8 context, including any global variables 57 | - For security reasons, be careful when evaluating user-provided code 58 | - Performance may be impacted when frequently evaluating code as compilation occurs each time 59 | -------------------------------------------------------------------------------- /runtime/v8/functions/eval/eval.go: -------------------------------------------------------------------------------- 1 | package eval 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/google/uuid" 8 | "github.com/yaoapp/gou/runtime/v8/bridge" 9 | "rogchap.com/v8go" 10 | ) 11 | 12 | // Eval execute an anonymous function 13 | 14 | var reFuncHead = regexp.MustCompile(`\s*function\s+(\w+)\s*\(([^)]*)\)\s*\{`) 15 | 16 | // ExportFunction function template 17 | func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate { 18 | return v8go.NewFunctionTemplate(iso, exec) 19 | } 20 | 21 | // exec 22 | func exec(info *v8go.FunctionCallbackInfo) *v8go.Value { 23 | 24 | jsArgs := info.Args() 25 | if len(jsArgs) < 1 { 26 | return bridge.JsException(info.Context(), "missing parameters") 27 | } 28 | 29 | if !jsArgs[0].IsString() { 30 | return bridge.JsException(info.Context(), "the first parameter should be a string") 31 | } 32 | 33 | args := []v8go.Valuer{} 34 | if len(jsArgs) > 1 { 35 | for _, arg := range jsArgs[1:] { 36 | args = append(args, arg) 37 | } 38 | } 39 | 40 | source := jsArgs[0].String() 41 | source = reFuncHead.ReplaceAllString(source, "($2) => {") 42 | name := fmt.Sprintf("__anonymous_%s", uuid.New().String()) 43 | 44 | iso := info.Context().Isolate() 45 | 46 | script, err := iso.CompileUnboundScript(source, name, v8go.CompileOptions{}) 47 | if err != nil { 48 | return bridge.JsException(info.Context(), err) 49 | } 50 | 51 | fn, err := script.Run(info.Context()) 52 | if err != nil { 53 | return bridge.JsException(info.Context(), err) 54 | } 55 | defer fn.Release() 56 | 57 | global := info.Context().Global() 58 | global.Set(name, fn) 59 | defer global.Delete(name) 60 | 61 | jsRes, err := global.MethodCall(name, args...) 62 | if err != nil { 63 | return bridge.JsException(info.Context(), err) 64 | } 65 | return jsRes 66 | } 67 | -------------------------------------------------------------------------------- /runtime/v8/functions/lang/lang.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/yaoapp/gou/lang" 7 | "github.com/yaoapp/gou/runtime/v8/bridge" 8 | "rogchap.com/v8go" 9 | ) 10 | 11 | // ExportFunction function template 12 | func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate { 13 | return v8go.NewFunctionTemplate(iso, replace) 14 | } 15 | 16 | // replace 17 | func replace(info *v8go.FunctionCallbackInfo) *v8go.Value { 18 | args := info.Args() 19 | if len(args) == 0 { 20 | return v8go.Undefined(info.Context().Isolate()) 21 | } 22 | 23 | if !args[0].IsString() { 24 | return args[0] 25 | } 26 | 27 | value := strings.TrimPrefix(args[0].String(), "::") 28 | lang.Replace(&value) 29 | 30 | jsValue, err := bridge.JsValue(info.Context(), value) 31 | if err != nil { 32 | return bridge.JsException(info.Context(), err) 33 | } 34 | 35 | return jsValue 36 | } 37 | -------------------------------------------------------------------------------- /runtime/v8/functions/lang/lang_test.go: -------------------------------------------------------------------------------- 1 | package lang 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/yaoapp/gou/runtime/v8/bridge" 8 | "rogchap.com/v8go" 9 | ) 10 | 11 | func TestReplace(t *testing.T) { 12 | 13 | ctx := prepare(t, false, "", nil) 14 | defer close(ctx) 15 | 16 | jsRes, err := ctx.RunScript(` 17 | const test = () => { 18 | const result = $L("::Pet"); 19 | return { 20 | result: result, 21 | __yao_global:__yao_global, 22 | __yao_sid: __yao_sid, 23 | __YAO_SU_ROOT: __YAO_SU_ROOT, 24 | } 25 | } 26 | test() 27 | `, "") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | goRes, err := bridge.GoValue(jsRes, ctx) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | res, ok := goRes.(map[string]interface{}) 38 | if !ok { 39 | t.Fatal("result error") 40 | } 41 | 42 | assert.Equal(t, "Pet", res["result"]) 43 | assert.Equal(t, nil, res["__yao_global"]) 44 | assert.Equal(t, false, res["__YAO_SU_ROOT"]) 45 | } 46 | 47 | func close(ctx *v8go.Context) { 48 | ctx.Isolate().Dispose() 49 | } 50 | 51 | func prepare(t *testing.T, root bool, sid string, global map[string]interface{}) *v8go.Context { 52 | 53 | iso := v8go.NewIsolate() 54 | 55 | template := v8go.NewObjectTemplate(iso) 56 | template.Set("$L", ExportFunction(iso)) 57 | 58 | ctx := v8go.NewContext(iso, template) 59 | 60 | var err error 61 | jsGlobal := v8go.Undefined(ctx.Isolate()) 62 | jsGlobal, err = bridge.JsValue(ctx, global) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | if err = ctx.Global().Set("__YAO_SU_ROOT", root); err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | if err = ctx.Global().Set("__yao_global", jsGlobal); err != nil { 72 | t.Fatal(err) 73 | } 74 | if err = ctx.Global().Set("__yao_sid", sid); err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | return ctx 79 | } 80 | -------------------------------------------------------------------------------- /runtime/v8/functions/process/process.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "github.com/yaoapp/gou/process" 5 | "github.com/yaoapp/gou/runtime/v8/bridge" 6 | "rogchap.com/v8go" 7 | ) 8 | 9 | // ExportFunction function template 10 | func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate { 11 | return v8go.NewFunctionTemplate(iso, exec) 12 | } 13 | 14 | // exec 15 | func exec(info *v8go.FunctionCallbackInfo) *v8go.Value { 16 | 17 | jsArgs := info.Args() 18 | if len(jsArgs) < 1 { 19 | return bridge.JsException(info.Context(), "missing parameters") 20 | } 21 | 22 | if !jsArgs[0].IsString() { 23 | return bridge.JsException(info.Context(), "the first parameter should be a string") 24 | } 25 | 26 | share, err := bridge.ShareData(info.Context()) 27 | if err != nil { 28 | return bridge.JsException(info.Context(), err) 29 | } 30 | 31 | goArgs := []interface{}{} 32 | if len(jsArgs) > 1 { 33 | for _, arg := range jsArgs[1:] { 34 | v, err := bridge.GoValue(arg, info.Context()) 35 | if err != nil { 36 | return bridge.JsException(info.Context(), err) 37 | } 38 | goArgs = append(goArgs, v) 39 | } 40 | } 41 | 42 | goRes, err := process.New(jsArgs[0].String(), goArgs...). 43 | WithGlobal(share.Global). 44 | WithSID(share.Sid). 45 | Exec() 46 | 47 | if err != nil { 48 | return bridge.JsException(info.Context(), err) 49 | } 50 | 51 | jsRes, err := bridge.JsValue(info.Context(), goRes) 52 | if err != nil { 53 | return bridge.JsException(info.Context(), err) 54 | } 55 | 56 | return jsRes 57 | } 58 | -------------------------------------------------------------------------------- /runtime/v8/functions/studio/studio.go: -------------------------------------------------------------------------------- 1 | package studio 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/yaoapp/gou/process" 8 | "github.com/yaoapp/gou/runtime/v8/bridge" 9 | "rogchap.com/v8go" 10 | ) 11 | 12 | // ExportFunction function template 13 | func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate { 14 | return v8go.NewFunctionTemplate(iso, exec) 15 | } 16 | 17 | // exec 18 | func exec(info *v8go.FunctionCallbackInfo) *v8go.Value { 19 | 20 | share, err := bridge.ShareData(info.Context()) 21 | if err != nil { 22 | return bridge.JsException(info.Context(), err) 23 | } 24 | 25 | if !share.Root { 26 | return bridge.JsException(info.Context(), "function is not allowed") 27 | } 28 | 29 | jsArgs := info.Args() 30 | if len(jsArgs) < 1 { 31 | return bridge.JsException(info.Context(), "missing parameters") 32 | } 33 | 34 | if !jsArgs[0].IsString() { 35 | return bridge.JsException(info.Context(), "the first parameter should be a string") 36 | } 37 | 38 | goArgs := []interface{}{} 39 | if len(jsArgs) > 1 { 40 | goArgs, err = bridge.GoValues(jsArgs[1:], info.Context()) 41 | if err != nil { 42 | return bridge.JsException(info.Context(), err) 43 | } 44 | } 45 | 46 | name := fmt.Sprintf("studio.%s", strings.TrimPrefix(jsArgs[0].String(), "studio.")) 47 | goRes, err := process.New(name, goArgs...). 48 | WithGlobal(share.Global). 49 | WithSID(share.Sid). 50 | Exec() 51 | 52 | if err != nil { 53 | return bridge.JsException(info.Context(), err) 54 | } 55 | 56 | jsRes, err := bridge.JsValue(info.Context(), goRes) 57 | if err != nil { 58 | return bridge.JsException(info.Context(), err) 59 | } 60 | 61 | return jsRes 62 | } 63 | -------------------------------------------------------------------------------- /runtime/v8/isolate_test.go: -------------------------------------------------------------------------------- 1 | package v8 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/yaoapp/kun/log" 8 | ) 9 | 10 | func TestSelectIsoStandard(t *testing.T) { 11 | option := option() 12 | option.Mode = "standard" 13 | option.HeapSizeLimit = 4294967296 14 | 15 | prepare(t, option) 16 | defer Stop() 17 | 18 | iso, err := SelectIsoStandard(time.Millisecond * 100) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | defer iso.Dispose() 23 | } 24 | 25 | // go test -bench=BenchmarkSelectIsoStandard 26 | // go test -bench=BenchmarkSelectIsoStandard -benchmem -benchtime=5s 27 | // go test -bench=BenchmarkSelectIsoStandard -benchtime=5s 28 | func BenchmarkSelectIsoStandard(b *testing.B) { 29 | option := option() 30 | option.Mode = "standard" 31 | option.HeapSizeLimit = 4294967296 32 | 33 | b.ResetTimer() 34 | var t *testing.T 35 | prepare(t, option) 36 | defer Stop() 37 | log.SetLevel(log.FatalLevel) 38 | 39 | // run the Call function b.N times 40 | for n := 0; n < b.N; n++ { 41 | iso, err := SelectIsoStandard(500 * time.Millisecond) 42 | if err != nil { 43 | b.Fatal(err) 44 | } 45 | iso.Dispose() 46 | } 47 | b.StopTimer() 48 | } 49 | 50 | func BenchmarkSelectIsoStandardPB(b *testing.B) { 51 | option := option() 52 | option.Mode = "standard" 53 | option.HeapSizeLimit = 4294967296 54 | 55 | b.ResetTimer() 56 | var t *testing.T 57 | prepare(t, option) 58 | defer Stop() 59 | log.SetLevel(log.FatalLevel) 60 | 61 | // run the Call function b.N times 62 | b.RunParallel(func(pb *testing.PB) { 63 | for pb.Next() { 64 | iso, err := SelectIsoStandard(500 * time.Millisecond) 65 | if err != nil { 66 | b.Fatal(err) 67 | } 68 | iso.Dispose() 69 | } 70 | }) 71 | b.StopTimer() 72 | } 73 | -------------------------------------------------------------------------------- /runtime/v8/objects/job/job_test.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/yaoapp/gou/process" 9 | "github.com/yaoapp/gou/runtime/v8/bridge" 10 | "rogchap.com/v8go" 11 | ) 12 | 13 | func TestJob(t *testing.T) { 14 | 15 | ctx := prepare(t, false, "", nil) 16 | defer close(ctx) 17 | 18 | jsRes, err := ctx.RunScript(` 19 | const test = () => { 20 | let job = new Job("test.job.run", "http://test.com", "foo=bar"); 21 | let progress = 0 22 | job.Pending(() => { 23 | progress ++ 24 | }); 25 | let data = job.Data() 26 | return {progress:progress, ...data}; 27 | } 28 | test() 29 | `, "") 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | goRes, err := bridge.GoValue(jsRes, ctx) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | res, ok := goRes.(map[string]interface{}) 40 | if !ok { 41 | t.Fatal("result error") 42 | } 43 | 44 | assert.Equal(t, "hello", res["message"]) 45 | assert.Equal(t, "http://test.com", res["url"]) 46 | assert.Equal(t, "foo=bar", res["payload"]) 47 | assert.Greater(t, res["progress"], float64(0)) 48 | } 49 | 50 | func close(ctx *v8go.Context) { 51 | ctx.Isolate().Dispose() 52 | } 53 | 54 | func prepare(t *testing.T, root bool, sid string, global map[string]interface{}) *v8go.Context { 55 | 56 | iso := v8go.NewIsolate() 57 | 58 | template := v8go.NewObjectTemplate(iso) 59 | template.Set("Job", New().ExportFunction(iso)) 60 | 61 | ctx := v8go.NewContext(iso, template) 62 | 63 | process.Register("test.job.run", func(process *process.Process) interface{} { 64 | time.Sleep(200 * time.Millisecond) 65 | url := process.ArgsString(0) 66 | payload := process.ArgsString(1) 67 | return map[string]interface{}{"message": "hello", "url": url, "payload": payload} 68 | }) 69 | 70 | return ctx 71 | } 72 | -------------------------------------------------------------------------------- /runtime/v8/objects/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yaoapp/gou/runtime/v8/bridge" 7 | "github.com/yaoapp/kun/log" 8 | "rogchap.com/v8go" 9 | ) 10 | 11 | // Object Javascript API 12 | type Object struct{} 13 | 14 | // New create a new Log Object 15 | func New() *Object { 16 | return &Object{} 17 | } 18 | 19 | // ExportObject Export as a Log Object 20 | // log.Trace("%s %v", "name", {"foo":"bar"} ) 21 | // log.Error("%s %v", "name", {"foo":"bar"} ) 22 | func (obj *Object) ExportObject(iso *v8go.Isolate) *v8go.ObjectTemplate { 23 | tmpl := v8go.NewObjectTemplate(iso) 24 | tmpl.Set("Trace", obj.run(iso, log.TraceLevel)) 25 | tmpl.Set("Debug", obj.run(iso, log.DebugLevel)) 26 | tmpl.Set("Info", obj.run(iso, log.InfoLevel)) 27 | tmpl.Set("Warn", obj.run(iso, log.WarnLevel)) 28 | tmpl.Set("Error", obj.run(iso, log.ErrorLevel)) 29 | tmpl.Set("Fatal", obj.run(iso, log.FatalLevel)) 30 | tmpl.Set("Panic", obj.run(iso, log.PanicLevel)) 31 | return tmpl 32 | } 33 | 34 | func (obj *Object) run(iso *v8go.Isolate, level log.Level) *v8go.FunctionTemplate { 35 | 36 | return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { 37 | 38 | args := info.Args() 39 | if len(args) < 1 { 40 | msg := fmt.Sprintf("Log: %s", "Missing parameters") 41 | log.Error(msg) 42 | return bridge.JsException(info.Context(), msg) 43 | } 44 | 45 | message := args[0].String() 46 | values := []interface{}{} 47 | var err error 48 | if len(args) > 1 { 49 | values, err = bridge.GoValues(args[1:], info.Context()) 50 | if err != nil { 51 | msg := fmt.Sprintf("Log: %s", err.Error()) 52 | log.Error(msg) 53 | return bridge.JsException(info.Context(), msg) 54 | } 55 | } 56 | 57 | switch level { 58 | case log.TraceLevel: 59 | log.Trace(message, values...) 60 | break 61 | case log.DebugLevel: 62 | log.Debug(message, values...) 63 | break 64 | case log.InfoLevel: 65 | log.Info(message, values...) 66 | break 67 | case log.WarnLevel: 68 | log.Warn(message, values...) 69 | break 70 | case log.ErrorLevel: 71 | log.Error(message, values...) 72 | break 73 | case log.FatalLevel: 74 | log.Fatal(message, values...) 75 | break 76 | case log.PanicLevel: 77 | log.Panic(message, values...) 78 | break 79 | default: 80 | log.Error(message, values...) 81 | } 82 | 83 | return v8go.Null(iso) 84 | }) 85 | } 86 | -------------------------------------------------------------------------------- /runtime/v8/objects/log/log_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/yaoapp/kun/log" 7 | "rogchap.com/v8go" 8 | ) 9 | 10 | func TestLogObject(t *testing.T) { 11 | 12 | iso := v8go.NewIsolate() 13 | defer iso.Dispose() 14 | 15 | obj := &Object{} 16 | global := v8go.NewObjectTemplate(iso) 17 | global.Set("log", obj.ExportObject(iso)) 18 | 19 | ctx := v8go.NewContext(iso, global) 20 | defer ctx.Close() 21 | 22 | log.SetLevel(log.TraceLevel) 23 | 24 | // ===== Trace 25 | _, err := ctx.RunScript(`log.Trace("Trace: %s %v %#v", "hello world", ["foo", "bar"], {"foo":"bar"})`, "") 26 | if err != nil { 27 | t.Fatal(err) 28 | } 29 | 30 | // ===== Debug 31 | _, err = ctx.RunScript(`log.Debug("Debug: %s %v %#v", "hello world", ["foo", "bar"], {"foo":"bar"})`, "") 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | // ===== Info 37 | _, err = ctx.RunScript(`log.Info("Info: %s %v %#v", "hello world", ["foo", "bar"], {"foo":"bar"})`, "") 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | // ===== Error 43 | _, err = ctx.RunScript(`log.Error("Error: %s %v %#v", "hello world", ["foo", "bar"], {"foo":"bar"})`, "") 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /runtime/v8/objects/time/time_test.go: -------------------------------------------------------------------------------- 1 | package time 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/yaoapp/gou/process" 9 | "github.com/yaoapp/kun/exception" 10 | "rogchap.com/v8go" 11 | ) 12 | 13 | func TestSleep(t *testing.T) { 14 | 15 | iso := v8go.NewIsolate() 16 | defer iso.Dispose() 17 | 18 | obj := &Object{} 19 | ctx := v8go.NewContext(iso) 20 | defer ctx.Close() 21 | obj.Set("time", ctx) 22 | 23 | _, err := ctx.RunScript(` 24 | const test = () => time.Sleep(200) 25 | test() 26 | `, "") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | } 32 | 33 | func TestAfter(t *testing.T) { 34 | iso := v8go.NewIsolate() 35 | defer iso.Dispose() 36 | 37 | obj := &Object{} 38 | ctx := v8go.NewContext(iso) 39 | defer ctx.Close() 40 | obj.Set("time", ctx) 41 | 42 | testArgs := []interface{}{} 43 | 44 | process.Register("unit.test.time", func(process *process.Process) interface{} { 45 | testArgs = process.Args 46 | return nil 47 | }) 48 | 49 | _, err := ctx.RunScript(` 50 | time.After(200, "unit.test.time", "foo", "bar") 51 | `, "") 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | 56 | time.Sleep(300 * time.Millisecond) 57 | assert.Equal(t, []interface{}{"foo", "bar"}, testArgs) 58 | 59 | } 60 | 61 | func TestAfterError(t *testing.T) { 62 | iso := v8go.NewIsolate() 63 | defer iso.Dispose() 64 | 65 | obj := &Object{} 66 | ctx := v8go.NewContext(iso) 67 | defer ctx.Close() 68 | obj.Set("time", ctx) 69 | 70 | testArgs := []interface{}{} 71 | 72 | process.Register("unit.test.time", func(process *process.Process) interface{} { 73 | testArgs = process.Args 74 | exception.New("unit.test.time", 200).Throw() 75 | return nil 76 | }) 77 | 78 | _, err := ctx.RunScript(` 79 | time.After(200, "unit.test.time", "foo", "bar") 80 | `, "") 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | time.Sleep(300 * time.Millisecond) 86 | assert.Equal(t, []interface{}{"foo", "bar"}, testArgs) 87 | 88 | } 89 | -------------------------------------------------------------------------------- /runtime/v8/objects/websocket/websocket_test.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/yaoapp/gou/websocket" 10 | "rogchap.com/v8go" 11 | ) 12 | 13 | func TestWebSocketPush(t *testing.T) { 14 | 15 | serve := serve(t) 16 | defer serve.Stop() 17 | 18 | iso := v8go.NewIsolate() 19 | defer iso.Dispose() 20 | 21 | ws := &WebSocket{} 22 | global := v8go.NewObjectTemplate(iso) 23 | global.Set("WebSocket", ws.ExportFunction(iso)) 24 | 25 | ctx := v8go.NewContext(iso, global) 26 | defer ctx.Close() 27 | 28 | v, err := ctx.RunScript(` 29 | var ws = new WebSocket("ws://127.0.0.1:5056/websocket/test", "po") 30 | ws.push("Hello World!") 31 | `, "") 32 | 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | assert.True(t, v.IsUndefined()) 38 | } 39 | 40 | func serve(t *testing.T) *websocket.Upgrader { 41 | 42 | ws, err := websocket.NewUpgrader("test") 43 | if err != nil { 44 | t.Fatalf("%s", err) 45 | } 46 | 47 | router := gin.Default() 48 | ws.SetHandler(func(message []byte, client int) ([]byte, error) { return message, nil }) 49 | ws.SetRouter(router) 50 | 51 | go ws.Start() 52 | go router.Run(":5056") 53 | time.Sleep(200 * time.Millisecond) 54 | return ws 55 | } 56 | -------------------------------------------------------------------------------- /runtime/v8/option.go: -------------------------------------------------------------------------------- 1 | package v8 2 | 3 | import "github.com/yaoapp/kun/log" 4 | 5 | // SetHeapAvailableSize set runtime Available 6 | func SetHeapAvailableSize(size uint) { 7 | runtimeOption.HeapAvailableSize = uint64(size) 8 | } 9 | 10 | // DisablePrecompile disable the precompile 11 | func DisablePrecompile() { 12 | runtimeOption.Precompile = false 13 | } 14 | 15 | // EnablePrecompile enable the precompile feature 16 | func EnablePrecompile() { 17 | runtimeOption.Precompile = true 18 | } 19 | 20 | // EnableDebug enable the debug mode 21 | func EnableDebug() { 22 | runtimeOption.Debug = true 23 | } 24 | 25 | // Validate the option 26 | func (option *Option) Validate() { 27 | 28 | if option.MinSize == 0 { 29 | option.MinSize = 50 30 | } 31 | 32 | if option.MaxSize == 0 { 33 | option.MaxSize = 100 34 | } 35 | 36 | if option.DefaultTimeout == 0 { 37 | option.DefaultTimeout = 200 38 | } 39 | 40 | if option.ContextTimeout == 0 { 41 | option.ContextTimeout = 200 42 | } 43 | 44 | if option.ContetxQueueSize == 0 { 45 | option.ContetxQueueSize = 10 46 | } 47 | 48 | if option.Mode == "" { 49 | option.Mode = "standard" 50 | } 51 | 52 | if option.MinSize > 500 { 53 | log.Warn("[V8] the maximum value of initSize is 500") 54 | option.MinSize = 500 55 | } 56 | 57 | if option.MaxSize > 500 { 58 | log.Warn("[V8] the maximum value of maxSize is 500") 59 | option.MaxSize = 500 60 | } 61 | 62 | if option.MinSize > option.MaxSize { 63 | log.Warn("[V8] the initSize value should smaller than maxSize") 64 | option.MaxSize = option.MinSize 65 | } 66 | 67 | if option.HeapSizeLimit == 0 { 68 | option.HeapSizeLimit = 1518338048 // 1.5G 69 | } 70 | 71 | if option.HeapSizeLimit > 4294967296 { 72 | log.Warn("[V8] the maximum value of HeapSizeLimit is 4294967296(4G)") 73 | option.HeapSizeLimit = 1518338048 // 1.5G 74 | } 75 | 76 | if option.HeapSizeRelease == 0 { 77 | option.HeapSizeRelease = 524288 // 50M 78 | } 79 | 80 | if option.HeapSizeRelease > 524288000 { 81 | log.Warn("[V8] the maximum value of heapSizeRelease is 524288000(500M)") 82 | option.HeapSizeRelease = 524288000 // 500M 83 | } 84 | 85 | if option.HeapAvailableSize == 0 { 86 | option.HeapAvailableSize = 524288000 // 500M 87 | } 88 | 89 | if option.HeapAvailableSize < 524288000 || option.HeapAvailableSize > option.HeapSizeLimit { 90 | log.Warn("[V8] the heapAvailableSize value is 524288000(500M) or heapSizeLimit * 0.30 to reduce the risk of program crashes") 91 | // option.HeapSizeRelease = 524288000 // 500M 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /runtime/v8/process.go: -------------------------------------------------------------------------------- 1 | package v8 2 | 3 | import ( 4 | "github.com/yaoapp/gou/process" 5 | "github.com/yaoapp/kun/exception" 6 | ) 7 | 8 | func init() { 9 | process.Register("scripts", processScripts) 10 | process.Register("studio", processStudio) 11 | } 12 | 13 | // processScripts 14 | func processScripts(process *process.Process) interface{} { 15 | 16 | script, err := Select(process.ID) 17 | if err != nil { 18 | exception.New("scripts.%s not loaded", 404, process.ID).Throw() 19 | return nil 20 | } 21 | 22 | return script.Exec(process) 23 | } 24 | 25 | // processScripts scripts.ID.Method 26 | func processStudio(process *process.Process) interface{} { 27 | 28 | script, err := SelectRoot(process.ID) 29 | if err != nil { 30 | exception.New("studio.%s not loaded", 404, process.ID).Throw() 31 | return nil 32 | } 33 | return script.Exec(process) 34 | 35 | } 36 | -------------------------------------------------------------------------------- /runtime/v8/process_test.go: -------------------------------------------------------------------------------- 1 | package v8 2 | 3 | // func TestProcessScripts(t *testing.T) { 4 | // prepare(t) 5 | // time.Sleep(20 * time.Millisecond) 6 | // assert.Equal(t, 2, isolates.Len) 7 | // assert.Equal(t, 2, len(chIsoReady)) 8 | 9 | // p, err := process.Of("scripts.runtime.basic.Hello", "world") 10 | // if err != nil { 11 | // t.Fatal(err) 12 | // } 13 | 14 | // value, err := p.Exec() 15 | // if err != nil { 16 | // t.Fatal(err) 17 | // } 18 | 19 | // assert.Equal(t, "world", value) 20 | 21 | // p, err = process.Of("scripts.runtime.basic.Error", "world") 22 | // if err != nil { 23 | // t.Fatal(err) 24 | // } 25 | 26 | // _, err = p.Exec() 27 | // assert.Contains(t, err.Error(), "at callStackTest") 28 | // assert.Contains(t, err.Error(), "at Error") 29 | 30 | // } 31 | 32 | // func TestProcessScriptsRoot(t *testing.T) { 33 | // prepare(t) 34 | // time.Sleep(20 * time.Millisecond) 35 | // assert.Equal(t, 2, isolates.Len) 36 | // assert.Equal(t, 2, len(chIsoReady)) 37 | 38 | // p, err := process.Of("studio.runtime.basic.Hello", "world") 39 | // if err != nil { 40 | // t.Fatal(err) 41 | // } 42 | 43 | // value, err := p.Exec() 44 | // if err != nil { 45 | // t.Fatal(err) 46 | // } 47 | 48 | // assert.Equal(t, "world", value) 49 | // } 50 | -------------------------------------------------------------------------------- /runtime/v8/require.go: -------------------------------------------------------------------------------- 1 | package v8 2 | 3 | import ( 4 | "github.com/yaoapp/gou/runtime/v8/bridge" 5 | "rogchap.com/v8go" 6 | ) 7 | 8 | // Require function template 9 | func Require(iso *v8go.Isolate) *v8go.FunctionTemplate { 10 | return v8go.NewFunctionTemplate(iso, func(info *v8go.FunctionCallbackInfo) *v8go.Value { 11 | 12 | share, err := bridge.ShareData(info.Context()) 13 | if err != nil { 14 | return bridge.JsException(info.Context(), err) 15 | } 16 | 17 | jsArgs := info.Args() 18 | if len(jsArgs) < 1 { 19 | return bridge.JsException(info.Context(), "missing parameters") 20 | } 21 | 22 | if !jsArgs[0].IsString() { 23 | return bridge.JsException(info.Context(), "the first parameter should be a string") 24 | } 25 | 26 | id := jsArgs[0].String() 27 | script := Scripts[id] 28 | if share.Root { 29 | if _, has := RootScripts[id]; has { 30 | script = RootScripts[id] 31 | } 32 | } 33 | 34 | globalName := "require" 35 | info.Context().RunScript(Transform(script.Source, globalName), script.File) 36 | global, _ := info.Context().Global().Get(globalName) 37 | return global 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /runtime/v8/require_test.go: -------------------------------------------------------------------------------- 1 | package v8 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/yaoapp/gou/runtime/v8/bridge" 8 | "rogchap.com/v8go" 9 | ) 10 | 11 | func TestRequre(t *testing.T) { 12 | 13 | option := option() 14 | option.Mode = "standard" 15 | option.HeapSizeLimit = 4294967296 16 | 17 | prepare(t, option) 18 | 19 | ctx := requrePrepare(t, false, "", nil) 20 | defer requireClose(ctx) 21 | 22 | jsRes, err := ctx.RunScript(` 23 | const lib = Require("runtime.lib"); 24 | const { Foo } = Require("runtime.lib"); 25 | function Hello() { 26 | return { 27 | "lib.Foo": lib.Foo(), 28 | "Foo": Foo() 29 | }; 30 | } 31 | Hello() 32 | `, "") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | goRes, err := bridge.GoValue(jsRes, ctx) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | res, ok := goRes.(map[string]interface{}) 43 | if !ok { 44 | t.Fatal("result error") 45 | } 46 | 47 | assert.Equal(t, "bar", res["lib.Foo"]) 48 | assert.Equal(t, "bar", res["Foo"]) 49 | } 50 | 51 | func requireClose(ctx *v8go.Context) { 52 | ctx.Isolate().Dispose() 53 | } 54 | 55 | func requrePrepare(t *testing.T, root bool, sid string, global map[string]interface{}) *v8go.Context { 56 | 57 | iso := v8go.NewIsolate() 58 | 59 | template := v8go.NewObjectTemplate(iso) 60 | template.Set("Require", Require(iso)) 61 | 62 | ctx := v8go.NewContext(iso, template) 63 | 64 | var err error 65 | goData := map[string]interface{}{ 66 | "SID": sid, 67 | "ROOT": root, 68 | "DATA": global, 69 | } 70 | 71 | jsData, err := bridge.JsValue(ctx, goData) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | if err = ctx.Global().Set("__yao_data", jsData); err != nil { 77 | t.Fatal(err) 78 | } 79 | 80 | return ctx 81 | } 82 | -------------------------------------------------------------------------------- /runtime/v8/sourcemap_test.go: -------------------------------------------------------------------------------- 1 | package v8 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | "testing" 7 | "time" 8 | 9 | "github.com/google/uuid" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/yaoapp/gou/application" 12 | "rogchap.com/v8go" 13 | ) 14 | 15 | func TestStackTrace(t *testing.T) { 16 | option := option() 17 | option.Mode = "standard" 18 | option.Import = true 19 | option.HeapSizeLimit = 4294967296 20 | option.Debug = true 21 | 22 | // add tsconfig 23 | tsconfig := &TSConfig{ 24 | CompilerOptions: &TSConfigCompilerOptions{ 25 | Paths: map[string][]string{ 26 | "@yao/*": {"./scripts/.types/*"}, 27 | "@lib/*": {"./scripts/runtime/ts/lib/*"}, 28 | }, 29 | }, 30 | } 31 | option.TSConfig = tsconfig 32 | 33 | prepare(t, option) 34 | defer Stop() 35 | 36 | files := map[string]string{ 37 | "page.ts": filepath.Join("scripts", "runtime", "ts", "page.ts"), 38 | "lib.hello.ts": filepath.Join("scripts", "runtime", "ts", "lib", "hello.ts"), 39 | } 40 | 41 | source, err := application.App.Read(files["page.ts"]) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | script, err := MakeScript(source, files["page.ts"], 5*time.Second) 47 | 48 | ctx, err := script.NewContext(uuid.New().String(), map[string]interface{}{}) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | defer ctx.Close() 53 | 54 | global := ctx.Global() 55 | _, err = global.MethodCall("SomethingError") 56 | if err == nil { 57 | t.Fatal("error expected but got nil") 58 | } 59 | 60 | e, ok := err.(*v8go.JSError) 61 | if !ok { 62 | t.Fatal("error is not a JSError") 63 | } 64 | 65 | trace := StackTrace(e, nil) 66 | assert.NotEmpty(t, trace) 67 | assert.Contains(t, trace, "400 Error occurred") 68 | assert.Contains(t, trace, "/scripts/runtime/ts/page.ts:12:2") 69 | assert.Contains(t, trace, "/scripts/runtime/ts/lib/bar.ts:7:2") 70 | assert.Contains(t, trace, "/scripts/runtime/ts/lib/err.ts:8:10") 71 | 72 | // with source root 73 | trace = StackTrace(e, map[string]string{"/scripts": "/iscripts"}) 74 | assert.NotEmpty(t, trace) 75 | assert.Contains(t, trace, "400 Error occurred") 76 | assert.Contains(t, trace, "/iscripts/runtime/ts/page.ts:12:2") 77 | assert.Contains(t, trace, "/iscripts/runtime/ts/lib/bar.ts:7:2") 78 | assert.Contains(t, trace, "/iscripts/runtime/ts/lib/err.ts:8:10") 79 | 80 | // with source root function 81 | replace := func(file string) string { 82 | if strings.HasPrefix(file, "/scripts") { 83 | return strings.Replace(file, "/scripts", "/fscripts", 1) 84 | } 85 | return file 86 | } 87 | 88 | trace = StackTrace(e, replace) 89 | assert.NotEmpty(t, trace) 90 | assert.Contains(t, trace, "400 Error occurred") 91 | assert.Contains(t, trace, "/fscripts/runtime/ts/page.ts:12:2") 92 | assert.Contains(t, trace, "/fscripts/runtime/ts/lib/bar.ts:7:2") 93 | assert.Contains(t, trace, "/fscripts/runtime/ts/lib/err.ts:8:10") 94 | } 95 | -------------------------------------------------------------------------------- /runtime/v8/store/cache.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | // Key the cache 4 | func (cache *Cache) Key() string { 5 | return cache.key 6 | } 7 | 8 | // Dispose the cache 9 | func (cache *Cache) Dispose() { 10 | for _, ctx := range cache.contexts { 11 | ctx.Context.Close() 12 | ctx = nil 13 | } 14 | 15 | // if iso, has := Isolates.Get(cache.key); has { 16 | // iso.Dispose() 17 | // iso = nil 18 | // } 19 | 20 | cache.contexts = nil 21 | cache = nil 22 | } 23 | -------------------------------------------------------------------------------- /runtime/v8/store/context.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "rogchap.com/v8go" 4 | 5 | var caches = New() 6 | 7 | // NewContext create a new context 8 | func NewContext(isolate, script string, ctx *v8go.Context) *Context { 9 | return &Context{ 10 | isolate: isolate, 11 | script: script, 12 | Context: ctx, 13 | } 14 | } 15 | 16 | // Release release the context 17 | func (ctx *Context) Release() error { 18 | // Remove the context from cache and release the context 19 | RemoveContextCache(ctx.isolate, ctx.script) 20 | return nil 21 | } 22 | 23 | // GetContextFromCache get the context from cache 24 | func GetContextFromCache(isolate, script string) (*Context, bool) { 25 | 26 | cache, has := caches.Get(isolate) 27 | if !has { 28 | return nil, false 29 | } 30 | 31 | ctx, has := cache.(*Cache).contexts[script] 32 | if !has { 33 | return nil, false 34 | } 35 | 36 | return ctx, true 37 | } 38 | 39 | // SetContextCache set the context to cache 40 | func SetContextCache(isolate, script string, ctx *Context) { 41 | 42 | cache, has := caches.Get(isolate) 43 | if !has { 44 | cache = &Cache{ 45 | key: isolate, 46 | contexts: map[string]*Context{}, 47 | } 48 | } 49 | cache.(*Cache).contexts[script] = ctx 50 | caches.Add(cache) 51 | } 52 | 53 | // RemoveContextCache remove the context cache 54 | func RemoveContextCache(isolate, script string) { 55 | cache, has := caches.Get(isolate) 56 | if !has { 57 | return 58 | } 59 | 60 | ctx, has := cache.(*Cache).contexts[script] 61 | if !has { 62 | return 63 | } 64 | 65 | ctx.Context.Close() 66 | ctx.Context = nil 67 | ctx = nil 68 | delete(cache.(*Cache).contexts, script) 69 | return 70 | } 71 | 72 | // MakeIsolateCache make the isolate cache 73 | func MakeIsolateCache(isolate string) { 74 | caches.Add(&Cache{ 75 | key: isolate, 76 | contexts: map[string]*Context{}, 77 | }) 78 | } 79 | 80 | // CleanIsolateCache clean the isolate cache 81 | func CleanIsolateCache(isolate string) { 82 | cache, has := caches.Get(isolate) 83 | if !has { 84 | return 85 | } 86 | cache.Dispose() 87 | caches.Remove(isolate) 88 | } 89 | -------------------------------------------------------------------------------- /runtime/v8/store/isolate.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | 9 | // IsoReady isolate is ready 10 | IsoReady uint8 = 0 11 | 12 | // IsoBusy isolate is in used 13 | IsoBusy uint8 = 1 14 | ) 15 | 16 | // Dispose the isolate 17 | func (iso *Isolate) Dispose() { 18 | // fmt.Printf("dispose isolate: %s\n", iso.Key()) 19 | Isolates.Remove(iso.Key()) // remove from normal isolates 20 | iso.Isolate.Dispose() 21 | iso.Isolate = nil 22 | iso.Template = nil 23 | iso = nil 24 | } 25 | 26 | // Key return the key of the isolate 27 | func (iso *Isolate) Key() string { 28 | return fmt.Sprintf("%p", iso) 29 | } 30 | 31 | // Lock the isolate 32 | func (iso *Isolate) Lock() { 33 | iso.Status = IsoBusy 34 | } 35 | 36 | // Unlock the isolate 37 | func (iso *Isolate) Unlock() { 38 | iso.Status = IsoReady 39 | } 40 | 41 | // Locked check if the isolate is locked 42 | func (iso *Isolate) Locked() bool { 43 | return iso.Status == IsoBusy 44 | } 45 | 46 | // Health check the isolate health 47 | func (iso *Isolate) Health(HeapSizeRelease uint64, HeapAvailableSize uint64) bool { 48 | 49 | // { 50 | // "ExternalMemory": 0, 51 | // "HeapSizeLimit": 1518338048, 52 | // "MallocedMemory": 16484, 53 | // "NumberOfDetachedContexts": 0, 54 | // "NumberOfNativeContexts": 3, 55 | // "PeakMallocedMemory": 24576, 56 | // "TotalAvailableSize": 1518051356, 57 | // "TotalHeapSize": 1261568, 58 | // "TotalHeapSizeExecutable": 262144, 59 | // "TotalPhysicalSize": 499164, 60 | // "UsedHeapSize": 713616 61 | // } 62 | 63 | if iso.Isolate == nil { 64 | return false 65 | } 66 | 67 | stat := iso.Isolate.GetHeapStatistics() 68 | if stat.TotalAvailableSize < HeapAvailableSize { // 500M 69 | return false 70 | } 71 | 72 | return true 73 | } 74 | -------------------------------------------------------------------------------- /runtime/v8/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Isolates the new isolate store 8 | var Isolates = New() 9 | 10 | // New create a new store 11 | func New() *Store { 12 | return &Store{data: map[string]IStore{}, mutex: &sync.Mutex{}} 13 | } 14 | 15 | // Get get a isolate 16 | func (store *Store) Get(key string) (IStore, bool) { 17 | store.mutex.Lock() 18 | defer store.mutex.Unlock() 19 | v, ok := store.data[key] 20 | if !ok { 21 | return nil, false 22 | } 23 | return v.(IStore), true 24 | } 25 | 26 | // Add a isolate 27 | func (store *Store) Add(data IStore) { 28 | store.mutex.Lock() 29 | defer store.mutex.Unlock() 30 | store.data[data.Key()] = data 31 | 32 | } 33 | 34 | // Remove a isolate 35 | func (store *Store) Remove(key string) { 36 | store.mutex.Lock() 37 | defer store.mutex.Unlock() 38 | delete(store.data, key) 39 | 40 | } 41 | 42 | // Len the length of store 43 | func (store *Store) Len() int { 44 | return len(store.data) 45 | } 46 | 47 | // Range traverse isolates 48 | func (store *Store) Range(callback func(data IStore) bool) { 49 | for _, v := range store.data { 50 | if !callback(v.(IStore)) { 51 | break 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /runtime/v8/store/types.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "sync" 5 | 6 | "rogchap.com/v8go" 7 | ) 8 | 9 | // Store the sync map 10 | type Store struct { 11 | data map[string]IStore 12 | mutex *sync.Mutex 13 | } 14 | 15 | // IStore the interface of store 16 | type IStore interface { 17 | Key() string 18 | Dispose() 19 | } 20 | 21 | // Isolate v8 Isolate 22 | type Isolate struct { 23 | *v8go.Isolate 24 | Status uint8 25 | Template *v8go.ObjectTemplate 26 | } 27 | 28 | // Context runtime context 29 | type Context struct { 30 | script string // Script ID 31 | isolate string // Isolate ID 32 | *v8go.Context 33 | } 34 | 35 | // Cache the cache 36 | type Cache struct { 37 | key string 38 | contexts map[string]*Context 39 | } 40 | -------------------------------------------------------------------------------- /runtime/v8/tests/plan_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/yaoapp/gou/application" 9 | "github.com/yaoapp/gou/process" 10 | v8 "github.com/yaoapp/gou/runtime/v8" 11 | "github.com/yaoapp/kun/utils" 12 | ) 13 | 14 | func TestPlan(t *testing.T) { 15 | option := v8.Option{} 16 | prepare(t, &option) 17 | defer v8.Stop() 18 | 19 | script, err := v8.Select("runtime.api.plan") 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | p := process.New("scripts.runtime.api.plan.Test") 25 | res := script.Exec(p) 26 | utils.Dump(res) 27 | } 28 | 29 | func prepare(t *testing.T, option *v8.Option) { 30 | root := os.Getenv("GOU_TEST_APPLICATION") 31 | 32 | // Load app 33 | app, err := application.OpenFromDisk(root) 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | application.Load(app) 38 | 39 | // application scripts 40 | scripts := map[string]string{ 41 | "runtime.api.plan": filepath.Join("scripts", "runtime", "api", "plan.ts"), 42 | } 43 | 44 | for id, file := range scripts { 45 | _, err := v8.Load(file, id) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | } 50 | 51 | prepareSetup(t, option) 52 | } 53 | 54 | func prepareSetup(t *testing.T, option *v8.Option) { 55 | v8.EnablePrecompile() 56 | v8.Start(option) 57 | } 58 | -------------------------------------------------------------------------------- /runtime/v8/tsconfig.go: -------------------------------------------------------------------------------- 1 | package v8 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/yaoapp/gou/application" 9 | ) 10 | 11 | // GetFileName get the file name from the tsconfig 12 | func (tsconfg *TSConfig) GetFileName(path string) (string, bool, error) { 13 | if tsconfg == nil { 14 | return path, false, nil 15 | } 16 | 17 | if tsconfg.CompilerOptions == nil || tsconfg.CompilerOptions.Paths == nil { 18 | return path, false, nil 19 | } 20 | 21 | for pattern, paths := range tsconfg.CompilerOptions.Paths { 22 | if tsconfg.Match(pattern, path) { 23 | f := tsconfg.ReplacePattern(path, pattern) 24 | for _, p := range paths { 25 | matched := false 26 | dir := filepath.Clean(filepath.Dir(p)) 27 | f = filepath.Join(dir, f) 28 | err := application.App.Walk(dir, func(root, filename string, isdir bool) error { 29 | if isdir { 30 | return nil 31 | } 32 | if filename == f { 33 | matched = true 34 | return filepath.SkipAll 35 | } 36 | return nil 37 | }, "*.ts") 38 | 39 | if matched { 40 | return f, true, nil 41 | } 42 | 43 | if err == nil { 44 | return path, false, nil 45 | } 46 | } 47 | } 48 | } 49 | return path, false, nil 50 | } 51 | 52 | // Match match the pattern 53 | func (tsconfg *TSConfig) Match(pattern, path string) bool { 54 | prefix := strings.Split(pattern, "/*")[0] + string(os.PathSeparator) 55 | return strings.HasPrefix(path, prefix) 56 | } 57 | 58 | // ReplacePattern replace the pattern 59 | func (tsconfg *TSConfig) ReplacePattern(path, pattern string) string { 60 | prefix := strings.Split(pattern, "/*")[0] 61 | file := strings.TrimPrefix(path, prefix) 62 | if strings.HasSuffix(file, ".ts") { 63 | return file 64 | } 65 | return file + ".ts" 66 | } 67 | -------------------------------------------------------------------------------- /runtime/v8/tsconfig_test.go: -------------------------------------------------------------------------------- 1 | package v8 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTSConfigGetFileName(t *testing.T) { 10 | option := option() 11 | option.Mode = "standard" 12 | option.Import = true 13 | option.HeapSizeLimit = 4294967296 14 | 15 | // add tsconfig 16 | tsconfig := &TSConfig{ 17 | CompilerOptions: &TSConfigCompilerOptions{ 18 | Paths: map[string][]string{ 19 | "@yao/*": {"./scripts/.types/*"}, 20 | "@lib/*": {"./scripts/runtime/ts/lib/*"}, 21 | }, 22 | }, 23 | } 24 | option.TSConfig = tsconfig 25 | prepare(t, option) 26 | defer Stop() 27 | 28 | file, match, err := tsconfig.GetFileName("@lib/foo") 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | if !match { 34 | t.Fatal("not match") 35 | } 36 | 37 | assert.Equal(t, "scripts/runtime/ts/lib/foo.ts", file) 38 | 39 | } 40 | -------------------------------------------------------------------------------- /runtime/v8/v8.go: -------------------------------------------------------------------------------- 1 | package v8 2 | 3 | import ( 4 | "github.com/yaoapp/gou/runtime/v8/store" 5 | ) 6 | 7 | var runtimeOption = &Option{} 8 | 9 | // Start v8 runtime 10 | func Start(option *Option) error { 11 | option.Validate() 12 | runtimeOption = option 13 | initialize() 14 | return nil 15 | } 16 | 17 | // Stop v8 runtime 18 | func Stop() { 19 | if isoReady != nil { 20 | close(isoReady) 21 | } 22 | isoReady = nil 23 | store.Isolates.Range(func(iso store.IStore) bool { 24 | key := iso.Key() 25 | store.CleanIsolateCache(key) 26 | store.Isolates.Remove(key) 27 | return true 28 | }) 29 | release() 30 | } 31 | -------------------------------------------------------------------------------- /runtime/v8/v8_test.go: -------------------------------------------------------------------------------- 1 | package v8 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/yaoapp/gou/application" 9 | ) 10 | 11 | func option() *Option { 12 | option := &Option{} 13 | option.Validate() 14 | return option 15 | } 16 | 17 | func prepare(t *testing.T, option *Option) { 18 | root := os.Getenv("GOU_TEST_APPLICATION") 19 | 20 | // Load app 21 | app, err := application.OpenFromDisk(root) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | application.Load(app) 26 | 27 | // application scripts 28 | scripts := map[string]string{ 29 | "runtime.basic": filepath.Join("scripts", "runtime", "basic.js"), 30 | "runtime.lib": filepath.Join("scripts", "runtime", "lib.js"), 31 | "runtime.typescript": filepath.Join("scripts", "runtime", "typescript.ts"), 32 | } 33 | 34 | for id, file := range scripts { 35 | _, err := Load(file, id) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | } 40 | 41 | // root scripts 42 | rootScripts := map[string]string{ 43 | "runtime.basic": filepath.Join("studio", "runtime", "basic.js"), 44 | } 45 | 46 | for id, file := range rootScripts { 47 | _, err := LoadRoot(file, id) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | } 52 | 53 | prepareSetup(t, option) 54 | } 55 | 56 | func prepareSetup(t *testing.T, option *Option) { 57 | EnablePrecompile() 58 | Start(option) 59 | } 60 | -------------------------------------------------------------------------------- /schema/schema.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "github.com/yaoapp/gou/schema/types" 5 | "github.com/yaoapp/gou/schema/xun" 6 | "github.com/yaoapp/xun/capsule" 7 | ) 8 | 9 | /** 10 | * Schema helpers and processes 11 | */ 12 | 13 | // Use pick a schema driver 14 | func Use(name string) types.Schema { 15 | switch name { 16 | case "tests": 17 | return &xun.Xun{} 18 | default: 19 | return &xun.Xun{ 20 | Option: xun.Option{Manager: capsule.Global}, 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /schema/xun/index.go: -------------------------------------------------------------------------------- 1 | package xun 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/yaoapp/gou/schema/types" 7 | "github.com/yaoapp/xun/dbal/schema" 8 | ) 9 | 10 | // setIndex 11 | func setIndex(table schema.Blueprint, index types.Index) error { 12 | if index.Name == "" { 13 | return fmt.Errorf("missing name %v", index) 14 | } 15 | 16 | if len(index.Columns) == 0 { 17 | return fmt.Errorf("index %s missing columns", index.Name) 18 | } 19 | 20 | switch index.Type { 21 | case "unique": 22 | table.AddUnique(index.Name, index.Columns...) 23 | return nil 24 | case "index": 25 | table.AddIndex(index.Name, index.Columns...) 26 | return nil 27 | case "primary": 28 | table.AddPrimary(index.Columns...) 29 | return nil 30 | case "fulltext": 31 | table.AddFulltext(index.Name, index.Columns...) 32 | return nil 33 | } 34 | return fmt.Errorf("Index %s, Type %s does not support", index.Name, index.Type) 35 | } 36 | -------------------------------------------------------------------------------- /server/http/types.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/yaoapp/gou/websocket" 9 | ) 10 | 11 | const ( 12 | // CREATED the server instance was created 13 | CREATED = uint8(iota) 14 | // STARTING the server instance is starting 15 | STARTING 16 | // READY the server instance is ready 17 | READY 18 | // RESTARTING the server instance is restarting 19 | RESTARTING 20 | // CLOSED the server instance was stopped 21 | CLOSED 22 | ) 23 | 24 | const ( 25 | // CLOSE close signal 26 | CLOSE = uint8(iota) + 5 27 | // RESTART restart signal 28 | RESTART 29 | // ERROR error signal 30 | ERROR 31 | ) 32 | 33 | // Option the http server opiton 34 | type Option struct { 35 | Port int `json:"port,omitempty"` 36 | Host string `json:"host,omitempty"` 37 | Timeout time.Duration `json:"timeout,omitempty"` 38 | Root string `json:"root,omitempty"` // API Root 39 | Allows []string `json:"allows,omitempty"` // CORS Domains 40 | } 41 | 42 | // Server the http server opiton 43 | type Server struct { 44 | router *gin.Engine 45 | addr net.Addr 46 | signal chan uint8 47 | event chan uint8 48 | status uint8 49 | option *Option 50 | upgraders map[string]*websocket.Upgrader 51 | } 52 | -------------------------------------------------------------------------------- /server/websocket/README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | # Refactor after v0.10.3 release 4 | -------------------------------------------------------------------------------- /server/websocket/websocket.type.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/yaoapp/gou/websocket" 4 | 5 | // WebSocket the websocket struct 6 | type WebSocket struct { 7 | websocket.WSClientOption 8 | Event WebSocketEvent `json:"event,omitempty"` 9 | Client *websocket.WSClient 10 | } 11 | 12 | // WebSocketEvent the websocket struct 13 | type WebSocketEvent struct { 14 | Data string `json:"data,omitempty"` 15 | Error string `json:"error,omitempty"` 16 | Closed string `json:"closed,omitempty"` 17 | Connected string `json:"connected,omitempty"` 18 | } 19 | -------------------------------------------------------------------------------- /server/websocket/websocket_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // func TestLoadWebSocket(t *testing.T) { 4 | // ws, err := LoadWebSocket(path.Join("websockets", "message.ws.json"), "message") 5 | // assert.Nil(t, err) 6 | // assert.Equal(t, ws.Name, "message") 7 | // assert.Equal(t, ws.URL, "ws://127.0.0.1:5011/websocket/message") 8 | // assert.Equal(t, ws.Protocols, []string{"yao-message-01"}) 9 | // assert.Equal(t, ws.Event.Data, "scripts.websocket.onData") 10 | // assert.Equal(t, ws.Event.Closed, "scripts.websocket.onClosed") 11 | // assert.Equal(t, ws.Event.Error, "scripts.websocket.onError") 12 | // assert.Equal(t, ws.Event.Connected, "scripts.websocket.onConnected") 13 | // } 14 | 15 | // func TestWebSocketOpen(t *testing.T) { 16 | // // err := Yao.Load(path.Join(TestScriptRoot, "websocket.js"), "websocket") 17 | // // if err != nil { 18 | // // t.Fatal(err) 19 | // // } 20 | 21 | // srv, url := serve(t) 22 | // defer srv.Stop() 23 | 24 | // LoadWebSocket(path.Join("websockets", "message.ws.json"), "message") 25 | // ws := SelectWebSocket("message") 26 | // err := ws.Open(url, "messageV2", "chatV3") 27 | // if err != nil { 28 | // t.Fatal(err) 29 | // } 30 | 31 | // } 32 | 33 | // func serve(t *testing.T) (*websocket.Upgrader, string) { 34 | 35 | // ws, err := websocket.NewUpgrader("test") 36 | // if err != nil { 37 | // t.Fatalf("%s", err) 38 | // } 39 | 40 | // gin.SetMode(gin.ReleaseMode) 41 | // router := gin.Default() 42 | // ws.SetHandler(func(message []byte, id int) ([]byte, error) { return message, nil }) 43 | // ws.SetRouter(router) 44 | 45 | // listener, err := net.Listen("tcp", "127.0.0.1:0") 46 | // if err != nil { 47 | // t.Fatal(err) 48 | // } 49 | 50 | // go ws.Start() 51 | // go func() { 52 | // http.Serve(listener, router) 53 | // }() 54 | // time.Sleep(200 * time.Millisecond) 55 | 56 | // return ws, fmt.Sprintf("ws://127.0.0.1:%d/websocket/test", listener.Addr().(*net.TCPAddr).Port) 57 | // } 58 | -------------------------------------------------------------------------------- /session/buntdb_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func init() { 11 | bunt, err := NewBuntDB("") 12 | if err != nil { 13 | panic(err) 14 | } 15 | Register("buntdb", bunt) 16 | } 17 | 18 | func TestBuntDBMake(t *testing.T) { 19 | s := Use("buntdb").Make().Expire(3600 * time.Second).AsGlobal() 20 | assert.NotNil(t, s.GetID()) 21 | } 22 | 23 | func TestBuntDBID(t *testing.T) { 24 | id := ID() 25 | s := Use("buntdb").ID(id) 26 | assert.Equal(t, id, s.GetID()) 27 | } 28 | 29 | func TestBuntDBMustSetGetDel(t *testing.T) { 30 | id := ID() 31 | s := Use("buntdb").ID(id).Expire(200 * time.Millisecond) 32 | s.MustSet("foo", "bar") 33 | v := s.MustGet("foo") 34 | assert.Equal(t, "bar", v) 35 | 36 | s.MustSetMany(map[string]interface{}{"hello": "world", "hi": "gou"}) 37 | assert.Equal(t, "world", s.MustGet("hello")) 38 | assert.Equal(t, "gou", s.MustGet("hi")) 39 | 40 | s.MustDel("hi") 41 | assert.Nil(t, s.MustGet("hi")) 42 | 43 | time.Sleep(201 * time.Millisecond) 44 | assert.Nil(t, s.MustGet("foo")) 45 | assert.Nil(t, s.MustGet("hello")) 46 | assert.Nil(t, s.MustGet("hi")) 47 | } 48 | 49 | func TestBuntDBMustSetWithEx(t *testing.T) { 50 | id := ID() 51 | ss := Use("buntdb").ID(id) 52 | ss.MustSetWithEx("foo", "bar", 200*time.Millisecond) 53 | assert.Equal(t, "bar", ss.MustGet("foo")) 54 | 55 | ss.MustSetManyWithEx(map[string]interface{}{"hello": "world", "hi": "gou"}, 200*time.Millisecond) 56 | assert.Equal(t, "world", ss.MustGet("hello")) 57 | assert.Equal(t, "gou", ss.MustGet("hi")) 58 | 59 | time.Sleep(201 * time.Millisecond) 60 | assert.Nil(t, ss.MustGet("foo")) 61 | assert.Nil(t, ss.MustGet("hello")) 62 | assert.Nil(t, ss.MustGet("hi")) 63 | } 64 | 65 | func TestBuntDBMustDump(t *testing.T) { 66 | id := ID() 67 | ss := Use("buntdb").ID(id).Expire(200 * time.Millisecond) 68 | ss.MustSet("foo", "bar") 69 | ss.MustSet("hello", "world") 70 | 71 | data := ss.MustDump() 72 | assert.Equal(t, "bar", data["foo"]) 73 | assert.Equal(t, "world", data["hello"]) 74 | 75 | time.Sleep(1000 * time.Millisecond) 76 | data = ss.MustDump() 77 | assert.Equal(t, map[string]interface{}{}, data) 78 | } 79 | -------------------------------------------------------------------------------- /session/redis_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func init() { 12 | 13 | host := os.Getenv("GOU_TEST_REDIS_HOST") 14 | port := os.Getenv("GOU_TEST_REDIS_PORT") 15 | db := os.Getenv("GOU_TEST_REDIS_DB") 16 | pass := os.Getenv("GOU_TEST_REDIS_PASSWORD") 17 | 18 | args := []string{} 19 | if port != "" { 20 | args = append(args, port) 21 | } 22 | 23 | if db != "" { 24 | args = append(args, db) 25 | } 26 | 27 | if pass != "" { 28 | args = append(args, pass) 29 | } 30 | 31 | rdb, err := NewRedis(host, args...) 32 | if err != nil { 33 | panic(err) 34 | } 35 | Register("redis", rdb) 36 | } 37 | 38 | func TestRedisMake(t *testing.T) { 39 | s := Use("redis").Make().Expire(3600 * time.Second).AsGlobal() 40 | assert.NotNil(t, s.GetID()) 41 | } 42 | 43 | func TestRedisID(t *testing.T) { 44 | id := ID() 45 | s := Use("redis").ID(id) 46 | assert.Equal(t, id, s.GetID()) 47 | } 48 | 49 | func TestRedisMustSetGetDel(t *testing.T) { 50 | id := ID() 51 | s := Use("redis").ID(id).Expire(200 * time.Millisecond) 52 | s.MustSet("foo", "bar") 53 | v := s.MustGet("foo") 54 | assert.Equal(t, "bar", v) 55 | 56 | s.MustSetMany(map[string]interface{}{"hello": "world", "hi": "gou"}) 57 | assert.Equal(t, "world", s.MustGet("hello")) 58 | assert.Equal(t, "gou", s.MustGet("hi")) 59 | 60 | s.MustDel("hi") 61 | assert.Nil(t, s.MustGet("hi")) 62 | 63 | time.Sleep(201 * time.Millisecond) 64 | assert.Nil(t, s.MustGet("foo")) 65 | assert.Nil(t, s.MustGet("hello")) 66 | assert.Nil(t, s.MustGet("hi")) 67 | } 68 | 69 | func TestRedisMustSetWithEx(t *testing.T) { 70 | id := ID() 71 | ss := Use("redis").ID(id) 72 | ss.MustSetWithEx("foo", "bar", 200*time.Millisecond) 73 | assert.Equal(t, "bar", ss.MustGet("foo")) 74 | 75 | ss.MustSetManyWithEx(map[string]interface{}{"hello": "world", "hi": "gou"}, 200*time.Millisecond) 76 | assert.Equal(t, "world", ss.MustGet("hello")) 77 | assert.Equal(t, "gou", ss.MustGet("hi")) 78 | 79 | time.Sleep(210 * time.Millisecond) 80 | assert.Nil(t, ss.MustGet("foo")) 81 | assert.Nil(t, ss.MustGet("hello")) 82 | assert.Nil(t, ss.MustGet("hi")) 83 | } 84 | 85 | func TestRedisMustDump(t *testing.T) { 86 | id := ID() 87 | ss := Use("redis").ID(id).Expire(200 * time.Millisecond) 88 | ss.MustSet("foo", "bar") 89 | ss.MustSet("hello", "world") 90 | 91 | data := ss.MustDump() 92 | assert.Equal(t, "bar", data["foo"]) 93 | assert.Equal(t, "world", data["hello"]) 94 | 95 | time.Sleep(201 * time.Millisecond) 96 | data = ss.MustDump() 97 | assert.Equal(t, map[string]interface{}{}, data) 98 | } 99 | -------------------------------------------------------------------------------- /session/types.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import "time" 4 | 5 | // Manager Session 管理器 6 | type Manager interface { 7 | Init() 8 | Set(id string, key string, value interface{}, expired time.Duration) error 9 | Get(id string, key string) (interface{}, error) 10 | Del(id string, key string) error 11 | Dump(id string) (map[string]interface{}, error) 12 | } 13 | 14 | // Session 数据结构 15 | type Session struct { 16 | id string 17 | name string 18 | timeout time.Duration 19 | Manager Manager 20 | } 21 | -------------------------------------------------------------------------------- /socket/README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | # Refactor after v0.10.3 release 4 | -------------------------------------------------------------------------------- /socket/server.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net" 7 | 8 | "github.com/yaoapp/kun/log" 9 | ) 10 | 11 | // Socket server (alpha -> will be refactored at a beta version...) 12 | 13 | // Start start socket server 14 | func Start(proto string, host string, port string, bufferSize int, KeepAlive int, handler func([]byte, int, error) ([]byte, error)) { 15 | if proto == "tcp" { 16 | tcpStart(host, port, bufferSize, KeepAlive, handler) 17 | } 18 | } 19 | 20 | // tcpStart start socket server with TCP/IP using TCP/IP protocol 21 | func tcpStart(host string, port string, bufferSize int, KeepAlive int, handler func([]byte, int, error) ([]byte, error)) error { 22 | listen, err := net.Listen("tcp", fmt.Sprintf("%s:%s", host, port)) 23 | if err != nil { 24 | log.Error("Start error: %s", err) 25 | return err 26 | } 27 | defer listen.Close() 28 | log.With(log.F{"bufferSize": bufferSize, "KeepAlive": KeepAlive}).Info("Listening ON: %s:%s", host, port) 29 | 30 | for { 31 | conn, err := listen.Accept() 32 | if err != nil { 33 | log.Error("Received error: %s", err) 34 | continue 35 | } 36 | log.With(log.F{"Remote": conn.RemoteAddr()}).Info("Connected") 37 | go handleRequest(conn, bufferSize, KeepAlive, handler) 38 | } 39 | } 40 | 41 | // handleRequest handel the client request 42 | func handleRequest(conn net.Conn, bufferSize int, KeepAlive int, handler func([]byte, int, error) ([]byte, error)) { 43 | clientAddr := conn.RemoteAddr().String() 44 | defer conn.Close() 45 | 46 | log.Trace("Connection success. Client address: %s", clientAddr) 47 | for { 48 | buffer := make([]byte, bufferSize) 49 | recvLen, err := conn.Read(buffer) 50 | if err != nil && err != io.EOF { 51 | log.Error("Read error: %s %s", err.Error(), clientAddr) 52 | break 53 | } 54 | 55 | if err == io.EOF { 56 | log.With(log.F{"Remote": conn.RemoteAddr()}).Info("Connection closed") 57 | break 58 | } 59 | 60 | log.Trace("Message received from Client %s %x", clientAddr, buffer) 61 | 62 | res, err := handler(buffer[:recvLen], recvLen, err) 63 | if err != nil { 64 | log.Error("Handler error: %s %s", err, clientAddr) 65 | break 66 | } 67 | 68 | log.Trace("Handler response %s %x", clientAddr, res) 69 | 70 | // Send Response to Server 71 | if res != nil { 72 | if _, err := conn.Write(res); err != nil { 73 | log.Error("Send error: %s %s", err, clientAddr) 74 | break 75 | } 76 | } 77 | 78 | if KeepAlive == -1 { 79 | log.With(log.F{"Remote": conn.RemoteAddr()}).Info("Close connection") 80 | break 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /socket/server_test.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | // func TestStart(t *testing.T) { 4 | // // Start("tcp", "0.0.0.0", "3019", 512, -1, func(data []byte, recvLen int, err error) ([]byte, error) { 5 | // // fmt.Printf("%x %d %v\n", data, recvLen, err) 6 | // // return []byte("pong"), nil 7 | // // }) 8 | // } 9 | 10 | // func TestStartKeepAlive(t *testing.T) { 11 | // // Start("tcp", "0.0.0.0", "3019", 512, 0, func(data []byte, recvLen int, err error) ([]byte, error) { 12 | // // fmt.Printf("%x %d %v\n", data, recvLen, err) 13 | // // return []byte("pong"), nil 14 | // // }) 15 | // } 16 | -------------------------------------------------------------------------------- /socket/socket.type.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | // Socket struct 4 | type Socket struct { 5 | Name string `json:"name"` 6 | Version string `json:"version"` 7 | Mode string `json:"mode,omitempty"` // Server | client 8 | Description string `json:"description,omitempty"` 9 | Protocol string `json:"protocol,omitempty"` 10 | Host string `json:"host,omitempty"` 11 | Port string `json:"port,omitempty"` 12 | Event Event `json:"event,omitempty"` 13 | Timeout int `json:"timeout,omitempty"` // timeout (seconds) 14 | BufferSize int `json:"buffer,omitempty"` // bufferSize 15 | KeepAlive int `json:"keep,omitempty"` // -1 not keep alive, 0 keep alive always, keep alive n seconds. 16 | Process string `json:"process,omitempty"` 17 | AttemptAfter int `json:"attempt_after,omitempty"` // Attempt attempt_after 18 | Attempts int `json:"attempts,omitempty"` // max times try to reconnect server when connection break (client mode only) 19 | client *Client 20 | } 21 | 22 | // Event struct 23 | type Event struct { 24 | Data string `json:"data,omitempty"` 25 | Error string `json:"error,omitempty"` 26 | Closed string `json:"closed,omitempty"` 27 | Connected string `json:"connected,omitempty"` 28 | } 29 | -------------------------------------------------------------------------------- /socket/socket_test.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | // func TestLoadSocket(t *testing.T) { 4 | // sock, err := Load(path.Join("sockets", "rfid.sock.json"), "rfid") 5 | // assert.Nil(t, err) 6 | // assert.Equal(t, sock.Name, "RFID Receiver (Server Mode)") 7 | // assert.Equal(t, sock.Mode, "server") 8 | // assert.Equal(t, sock.Event.Data, "scripts.socket.onData") 9 | // assert.Equal(t, sock.Event.Closed, "scripts.socket.onClosed") 10 | // assert.Equal(t, sock.Event.Error, "scripts.socket.onError") 11 | // assert.Equal(t, sock.Event.Connected, "scripts.socket.onConnected") 12 | // assert.Equal(t, sock.Port, "3019") 13 | // assert.Equal(t, sock.Host, "0.0.0.0") 14 | // } 15 | 16 | // func TestSocketStart(t *testing.T) { 17 | // Load(path.Join("sockets", "rfid.sock.json"), "rfid") 18 | // sock := Select("rfid") 19 | // assert.Equal(t, sock.Name, "RFID Receiver (Server Mode)") 20 | // assert.Equal(t, sock.Mode, "server") 21 | // assert.Equal(t, sock.Event.Data, "scripts.socket.onData") 22 | // assert.Equal(t, sock.Event.Closed, "scripts.socket.onClosed") 23 | // assert.Equal(t, sock.Event.Error, "scripts.socket.onError") 24 | // assert.Equal(t, sock.Event.Connected, "scripts.socket.onConnected") 25 | // assert.Equal(t, sock.Port, "3019") 26 | // assert.Equal(t, sock.Host, "0.0.0.0") 27 | 28 | // // sock.Start() 29 | // } 30 | 31 | // func TestSocketConnect(t *testing.T) { 32 | // Load(path.Join("sockets", "rfid_client.sock.json"), "rfid_client") 33 | // sock := Select("rfid_client") 34 | // assert.Equal(t, sock.Name, "RFID Receiver (Client Mode)") 35 | // assert.Equal(t, sock.Mode, "client") 36 | // assert.Equal(t, sock.Event.Data, "scripts.socket.onData") 37 | // assert.Equal(t, sock.Event.Error, "scripts.socket.onError") 38 | // assert.Equal(t, sock.Event.Closed, "scripts.socket.onClosed") 39 | // assert.Equal(t, sock.Event.Connected, "scripts.socket.onConnected") 40 | // assert.Equal(t, sock.Port, "3019") 41 | // assert.Equal(t, sock.Host, "192.168.31.33") 42 | 43 | // // sock.Connect() 44 | // } 45 | 46 | // func TestSocketOpen(t *testing.T) { 47 | // // err := Yao.Load(path.Join(TestScriptRoot, "socket.js"), "socket") 48 | // // if err != nil { 49 | // // t.Fatal(err) 50 | // // } 51 | // // LoadSocket(path.Join("sockets", "rfid_client.sock.json"), "rfid_client") 52 | // // sock := SelectSocket("rfid_client") 53 | // // sock.Open() 54 | // } 55 | -------------------------------------------------------------------------------- /socket/types.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "net" 5 | "time" 6 | ) 7 | 8 | const ( 9 | // WAITING waiting for connecting 10 | WAITING uint = iota 11 | 12 | // CONNECTING connecting the host 13 | CONNECTING 14 | 15 | // CONNECTED the socket is connected 16 | CONNECTED 17 | 18 | // CLOSED the socket is closed 19 | CLOSED 20 | ) 21 | 22 | const ( 23 | 24 | // MREAD socket read error ( the local peer closed ) 25 | MREAD uint = iota 26 | 27 | // MBREAK the remote peer closed 28 | MBREAK 29 | 30 | // MCLOSE user send the CLOSE signal 31 | MCLOSE 32 | ) 33 | 34 | // Client the socket client 35 | type Client struct { 36 | Status uint 37 | Conn net.Conn 38 | Option Option 39 | Handlers Handlers 40 | Attempts int 41 | AttemptAfter time.Duration 42 | AttemptTimes int 43 | } 44 | 45 | // Option the socket option 46 | type Option struct { 47 | Protocol string `json:"protocol,omitempty"` // TCP/UDP 48 | Host string `json:"host,omitempty"` 49 | Port string `json:"port,omitempty"` 50 | Timeout time.Duration `json:"timeout,omitempty"` // timeout (seconds) 51 | BufferSize int `json:"buffer,omitempty"` // bufferSize 52 | KeepAlive time.Duration `json:"keep,omitempty"` // -1 not keep alive, 0 keep alive always, keep alive n seconds. 53 | AttemptAfter time.Duration `json:"attempt_after,omitempty"` // Attempt attempt_after 54 | Attempts int `json:"attempts,omitempty"` // max times try to reconnect server when connection break (client mode only) 55 | } 56 | 57 | // Handlers the socket handlers 58 | type Handlers struct { 59 | Data DataHandler 60 | Error ErrorHandler 61 | Closed ClosedHandler 62 | Connected ConnectedHandler 63 | } 64 | 65 | // DataHandler Handler 66 | type DataHandler func([]byte, int) ([]byte, error) 67 | 68 | // ErrorHandler Handler 69 | type ErrorHandler func(error) 70 | 71 | // ClosedHandler Handler 72 | type ClosedHandler func([]byte, error) []byte 73 | 74 | // ConnectedHandler Handler 75 | type ConnectedHandler func(option Option) error 76 | -------------------------------------------------------------------------------- /ssl/certificate.go: -------------------------------------------------------------------------------- 1 | package ssl 2 | 3 | import ( 4 | "crypto/x509" 5 | "encoding/pem" 6 | "fmt" 7 | 8 | "github.com/yaoapp/gou/application" 9 | ) 10 | 11 | // Certificates loaded 12 | var Certificates = map[string]*Certificate{} 13 | 14 | // Load Load the certificate from the given file 15 | func Load(file string, name string) (*Certificate, error) { 16 | data, err := application.App.Read(file) 17 | if err != nil { 18 | return nil, fmt.Errorf("read certificate pem file err:%s", err.Error()) 19 | } 20 | return LoadCertificate(data, name) 21 | } 22 | 23 | // LoadCertificate load the certificate from the given file 24 | func LoadCertificate(data []byte, name string) (*Certificate, error) { 25 | cert, err := NewCertificate(data) 26 | if err != nil { 27 | return nil, err 28 | } 29 | Certificates[name] = cert 30 | return cert, nil 31 | } 32 | 33 | // NewCertificate creat a new certificate from file 34 | func NewCertificate(data []byte) (*Certificate, error) { 35 | block, _ := pem.Decode(data) 36 | if block == nil { 37 | return nil, fmt.Errorf("decode certificate error") 38 | } 39 | 40 | switch block.Type { 41 | case "CERTIFICATE": 42 | cert, err := x509.ParseCertificate(block.Bytes) 43 | if err != nil { 44 | return nil, fmt.Errorf("parse certificate err:%s", err.Error()) 45 | } 46 | return &Certificate{cert: cert, pub: cert.PublicKey, data: data, Type: block.Type, Format: "PEM"}, nil 47 | 48 | case "PRIVATE KEY": 49 | key, err := x509.ParsePKCS8PrivateKey(block.Bytes) 50 | if err != nil { 51 | return nil, fmt.Errorf("parse certificate err:%s", err.Error()) 52 | } 53 | return &Certificate{pri: key, data: data, Type: block.Type, Format: "PEM"}, nil 54 | 55 | case "PUBLIC KEY": 56 | key, err := x509.ParsePKIXPublicKey(block.Bytes) 57 | if err != nil { 58 | return nil, fmt.Errorf("parse certificate err:%s", err.Error()) 59 | } 60 | return &Certificate{pub: key, data: data, Type: block.Type, Format: "PEM"}, nil 61 | } 62 | 63 | return nil, fmt.Errorf("the kind of PEM should be CERTIFICATE") 64 | 65 | } 66 | -------------------------------------------------------------------------------- /ssl/certificate_test.go: -------------------------------------------------------------------------------- 1 | package ssl 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/yaoapp/gou/application" 10 | ) 11 | 12 | func TestLoadCertificateFrom(t *testing.T) { 13 | prepare(t) 14 | cert, err := Load(certFile("cert.pem"), "cert") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | assert.Equal(t, "CERTIFICATE", cert.Type) 19 | assert.NotNil(t, cert.cert) 20 | 21 | _, has := Certificates["cert"] 22 | assert.True(t, has) 23 | } 24 | 25 | func TestLoadCertificateFromPrivate(t *testing.T) { 26 | prepare(t) 27 | cert, err := Load(certFile("private.pem"), "private") 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | assert.Equal(t, "PRIVATE KEY", cert.Type) 32 | assert.NotNil(t, cert.pri) 33 | 34 | _, has := Certificates["private"] 35 | assert.True(t, has) 36 | } 37 | 38 | func TestLoadCertificateFromPublic(t *testing.T) { 39 | prepare(t) 40 | cert, err := Load(certFile("public.pem"), "public") 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | assert.Equal(t, "PUBLIC KEY", cert.Type) 45 | assert.NotNil(t, cert.pub) 46 | 47 | _, has := Certificates["public"] 48 | assert.True(t, has) 49 | } 50 | 51 | func prepare(t *testing.T) { 52 | root := os.Getenv("GOU_TEST_APPLICATION") 53 | app, err := application.OpenFromDisk(root) // Load app 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | application.Load(app) 58 | } 59 | 60 | func certFile(name string) string { 61 | return path.Join("certs", name) 62 | } 63 | -------------------------------------------------------------------------------- /ssl/process.go: -------------------------------------------------------------------------------- 1 | package ssl 2 | 3 | import ( 4 | "github.com/yaoapp/gou/process" 5 | "github.com/yaoapp/kun/exception" 6 | ) 7 | 8 | func init() { 9 | process.Register("ssl.sign", ProcessSign) 10 | process.Register("ssl.verify", ProcessVerify) 11 | } 12 | 13 | // ProcessSign computes a signature for the specified data by generating a cryptographic digital signature 14 | func ProcessSign(process *process.Process) interface{} { 15 | process.ValidateArgNums(3) 16 | data := process.ArgsString(0) 17 | certName := process.ArgsString(1) 18 | algorithm := process.ArgsString(2) 19 | 20 | cert, has := Certificates[certName] 21 | if !has { 22 | exception.New("cert %s does not load ", 400, certName).Throw() 23 | } 24 | 25 | sign, err := SignStrBase64(data, cert, algorithm) 26 | if err != nil { 27 | exception.New("%s", 500, err).Throw() 28 | } 29 | 30 | return sign 31 | } 32 | 33 | // ProcessVerify verifies that the signature is correct for the specified data 34 | func ProcessVerify(process *process.Process) interface{} { 35 | process.ValidateArgNums(4) 36 | data := process.ArgsString(0) 37 | sign := process.ArgsString(1) 38 | certName := process.ArgsString(2) 39 | algorithm := process.ArgsString(3) 40 | 41 | cert, has := Certificates[certName] 42 | if !has { 43 | exception.New("cert %s does not load", 400, certName).Throw() 44 | } 45 | 46 | res, err := VerifyStrBase64(data, sign, cert, algorithm) 47 | if err != nil { 48 | exception.New("%s", 500, err).Throw() 49 | } 50 | 51 | return res 52 | } 53 | -------------------------------------------------------------------------------- /ssl/process_test.go: -------------------------------------------------------------------------------- 1 | package ssl 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/yaoapp/gou/process" 8 | ) 9 | 10 | func TestProcessSign(t *testing.T) { 11 | prepare(t) 12 | _, err := Load(certFile("private.pem"), "private") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | args := []interface{}{"hello world", "private", "SHA256"} 18 | signature, err := process.New("ssl.Sign", args...).Exec() 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | assert.Equal(t, "EDHf3C9TXEk7y8LzIk5czLefXZyGxcMDVMcbNuBBegDkTqnPsRQnhFtNOgCdox8lI3MzLatwjoljoMY4Qk+sHGd5mAHMpiREa1gRFSVYpA2xvXZ3+KsfOHAdICQrfUdy59QaJGo6iGPNGG8PQOXHPTVNn6LMfryat9+f4l21DPAZiT0RyCUgFZE3/Qv8Z/6J4AsIXMSKZD6BGPPHUxGe7UBrXZvcR5dX25EiNjuH2OO38YJnDiTRVw14UI5fk/mQrwRdezj5tSKFCyHt912BZExXtkHISiYFNTZ/2RhOup5Xx6o3GvrEOdshrnN80Lwu1Aaju+lnZp13hDz4P6hU7w==", signature) 23 | } 24 | 25 | func TestProcessVerify(t *testing.T) { 26 | _, err := Load(certFile("cert.pem"), "cert") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | signature := "EDHf3C9TXEk7y8LzIk5czLefXZyGxcMDVMcbNuBBegDkTqnPsRQnhFtNOgCdox8lI3MzLatwjoljoMY4Qk+sHGd5mAHMpiREa1gRFSVYpA2xvXZ3+KsfOHAdICQrfUdy59QaJGo6iGPNGG8PQOXHPTVNn6LMfryat9+f4l21DPAZiT0RyCUgFZE3/Qv8Z/6J4AsIXMSKZD6BGPPHUxGe7UBrXZvcR5dX25EiNjuH2OO38YJnDiTRVw14UI5fk/mQrwRdezj5tSKFCyHt912BZExXtkHISiYFNTZ/2RhOup5Xx6o3GvrEOdshrnN80Lwu1Aaju+lnZp13hDz4P6hU7w==" 32 | args := []interface{}{"hello world", signature, "cert", "SHA256"} 33 | res, err := process.New("ssl.Verify", args...).Exec() 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | 38 | assert.True(t, res.(bool)) 39 | } 40 | -------------------------------------------------------------------------------- /ssl/ssl_test.go: -------------------------------------------------------------------------------- 1 | package ssl 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSignStrBase64(t *testing.T) { 11 | prepare(t) 12 | cert, err := Load(certFile("private.pem"), "private") 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | 17 | signature, err := SignStrBase64("hello world", cert, "SHA256") 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | assert.Equal(t, "EDHf3C9TXEk7y8LzIk5czLefXZyGxcMDVMcbNuBBegDkTqnPsRQnhFtNOgCdox8lI3MzLatwjoljoMY4Qk+sHGd5mAHMpiREa1gRFSVYpA2xvXZ3+KsfOHAdICQrfUdy59QaJGo6iGPNGG8PQOXHPTVNn6LMfryat9+f4l21DPAZiT0RyCUgFZE3/Qv8Z/6J4AsIXMSKZD6BGPPHUxGe7UBrXZvcR5dX25EiNjuH2OO38YJnDiTRVw14UI5fk/mQrwRdezj5tSKFCyHt912BZExXtkHISiYFNTZ/2RhOup5Xx6o3GvrEOdshrnN80Lwu1Aaju+lnZp13hDz4P6hU7w==", signature) 22 | } 23 | 24 | func TestSignHexBase64(t *testing.T) { 25 | prepare(t) 26 | cert, err := Load(certFile("private.pem"), "private") 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | hexstr := hex.EncodeToString([]byte("hello world")) 31 | signature, err := SignHexBase64(hexstr, cert, "SHA256") 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | assert.Equal(t, "EDHf3C9TXEk7y8LzIk5czLefXZyGxcMDVMcbNuBBegDkTqnPsRQnhFtNOgCdox8lI3MzLatwjoljoMY4Qk+sHGd5mAHMpiREa1gRFSVYpA2xvXZ3+KsfOHAdICQrfUdy59QaJGo6iGPNGG8PQOXHPTVNn6LMfryat9+f4l21DPAZiT0RyCUgFZE3/Qv8Z/6J4AsIXMSKZD6BGPPHUxGe7UBrXZvcR5dX25EiNjuH2OO38YJnDiTRVw14UI5fk/mQrwRdezj5tSKFCyHt912BZExXtkHISiYFNTZ/2RhOup5Xx6o3GvrEOdshrnN80Lwu1Aaju+lnZp13hDz4P6hU7w==", signature) 36 | } 37 | 38 | func TestCertVerifyStrBase64(t *testing.T) { 39 | prepare(t) 40 | cert, err := Load(certFile("cert.pem"), "cert") 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | signature := "EDHf3C9TXEk7y8LzIk5czLefXZyGxcMDVMcbNuBBegDkTqnPsRQnhFtNOgCdox8lI3MzLatwjoljoMY4Qk+sHGd5mAHMpiREa1gRFSVYpA2xvXZ3+KsfOHAdICQrfUdy59QaJGo6iGPNGG8PQOXHPTVNn6LMfryat9+f4l21DPAZiT0RyCUgFZE3/Qv8Z/6J4AsIXMSKZD6BGPPHUxGe7UBrXZvcR5dX25EiNjuH2OO38YJnDiTRVw14UI5fk/mQrwRdezj5tSKFCyHt912BZExXtkHISiYFNTZ/2RhOup5Xx6o3GvrEOdshrnN80Lwu1Aaju+lnZp13hDz4P6hU7w==" 46 | res, err := VerifyStrBase64("hello world", signature, cert, "SHA256") 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | assert.True(t, res) 52 | } 53 | 54 | func TestCertVerifyStrBase64Public(t *testing.T) { 55 | prepare(t) 56 | cert, err := Load(certFile("public.pem"), "public") 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | signature := "EDHf3C9TXEk7y8LzIk5czLefXZyGxcMDVMcbNuBBegDkTqnPsRQnhFtNOgCdox8lI3MzLatwjoljoMY4Qk+sHGd5mAHMpiREa1gRFSVYpA2xvXZ3+KsfOHAdICQrfUdy59QaJGo6iGPNGG8PQOXHPTVNn6LMfryat9+f4l21DPAZiT0RyCUgFZE3/Qv8Z/6J4AsIXMSKZD6BGPPHUxGe7UBrXZvcR5dX25EiNjuH2OO38YJnDiTRVw14UI5fk/mQrwRdezj5tSKFCyHt912BZExXtkHISiYFNTZ/2RhOup5Xx6o3GvrEOdshrnN80Lwu1Aaju+lnZp13hDz4P6hU7w==" 62 | res, err := VerifyStrBase64("hello world", signature, cert, "SHA256") 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | assert.True(t, res) 68 | } 69 | -------------------------------------------------------------------------------- /ssl/types.go: -------------------------------------------------------------------------------- 1 | package ssl 2 | 3 | import "crypto/x509" 4 | 5 | // Certificate the ssl Certificate 6 | type Certificate struct { 7 | cert *x509.Certificate 8 | pub any 9 | pri any 10 | Type string 11 | Format string // PEM 12 | data []byte 13 | } 14 | -------------------------------------------------------------------------------- /store/file/README.md: -------------------------------------------------------------------------------- 1 | # File KV-Store 2 | -------------------------------------------------------------------------------- /store/lru/README.md: -------------------------------------------------------------------------------- 1 | # LRU Cache 2 | -------------------------------------------------------------------------------- /store/lru/types.go: -------------------------------------------------------------------------------- 1 | package lru 2 | 3 | import lru "github.com/hashicorp/golang-lru" 4 | 5 | // Cache lru cache 6 | type Cache struct { 7 | size int 8 | lru *lru.ARCCache 9 | } 10 | -------------------------------------------------------------------------------- /store/mongo/README.md: -------------------------------------------------------------------------------- 1 | # MongoDB KV-Store 2 | -------------------------------------------------------------------------------- /store/mongo/types.go: -------------------------------------------------------------------------------- 1 | package mongo 2 | 3 | import ( 4 | "time" 5 | 6 | "go.mongodb.org/mongo-driver/mongo" 7 | ) 8 | 9 | // Store redis store 10 | type Store struct { 11 | Database *mongo.Database 12 | Collection *mongo.Collection 13 | Option Option 14 | } 15 | 16 | // Option redis option 17 | type Option struct { 18 | Timeout time.Duration 19 | } 20 | -------------------------------------------------------------------------------- /store/process.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/yaoapp/gou/process" 7 | "github.com/yaoapp/kun/log" 8 | ) 9 | 10 | // StoreHandlers store process handlers 11 | var StoreHandlers = map[string]process.Handler{ 12 | "get": processStoreGet, 13 | "set": processStoreSet, 14 | "has": processStoreHas, 15 | "del": processStoreDel, 16 | "getdel": processStoreGetDel, 17 | "len": processStoreLen, 18 | "keys": processStoreKeys, 19 | "clear": processStoreClear, 20 | } 21 | 22 | func init() { 23 | process.RegisterGroup("stores", StoreHandlers) 24 | } 25 | 26 | // processStoreGet stores..Get 27 | func processStoreGet(process *process.Process) interface{} { 28 | process.ValidateArgNums(1) 29 | store := Select(process.ID) 30 | value, ok := store.Get(process.ArgsString(0)) 31 | if !ok { 32 | log.Error("store %s Get return false", process.ID) 33 | return nil 34 | } 35 | return value 36 | } 37 | 38 | // processStoreSet stores..Set 39 | func processStoreSet(process *process.Process) interface{} { 40 | process.ValidateArgNums(2) 41 | store := Select(process.ID) 42 | duration := process.ArgsInt(2, 0) 43 | store.Set(process.ArgsString(0), process.Args[1], time.Duration(duration)*time.Second) 44 | return nil 45 | } 46 | 47 | // processStoreHas stores..Has 48 | func processStoreHas(process *process.Process) interface{} { 49 | process.ValidateArgNums(1) 50 | store := Select(process.ID) 51 | return store.Has(process.ArgsString(0)) 52 | } 53 | 54 | // processStoreDel stores..Del 55 | func processStoreDel(process *process.Process) interface{} { 56 | process.ValidateArgNums(1) 57 | store := Select(process.ID) 58 | store.Del(process.ArgsString(0)) 59 | return nil 60 | } 61 | 62 | // processStoreGetDel stores..GetDel 63 | func processStoreGetDel(process *process.Process) interface{} { 64 | process.ValidateArgNums(1) 65 | store := Select(process.ID) 66 | value, ok := store.GetDel(process.ArgsString(0)) 67 | if !ok { 68 | log.Error("store %s GetDel return false", process.ID) 69 | return nil 70 | } 71 | return value 72 | } 73 | 74 | // processStoreLen stores..Len 75 | func processStoreLen(process *process.Process) interface{} { 76 | store := Select(process.ID) 77 | return store.Len() 78 | } 79 | 80 | // processStoreKeys stores..Keys 81 | func processStoreKeys(process *process.Process) interface{} { 82 | store := Select(process.ID) 83 | return store.Keys() 84 | } 85 | 86 | // processStoreClear stores..Keys 87 | func processStoreClear(process *process.Process) interface{} { 88 | store := Select(process.ID) 89 | store.Clear() 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /store/process_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/yaoapp/gou/process" 9 | ) 10 | 11 | func TestStoreProcess(t *testing.T) { 12 | prepare(t) 13 | prepareStores(t) 14 | testStoreProcess(t, "cache") 15 | testStoreProcess(t, "share") 16 | testStoreProcess(t, "data") 17 | } 18 | 19 | func testStoreProcess(t *testing.T, name string) { 20 | 21 | process.New(fmt.Sprintf("stores.%s.Clear", name)).Run() 22 | value := process.New(fmt.Sprintf("stores.%s.Len", name)).Run() 23 | assert.Equal(t, 0, value) 24 | 25 | assert.NotPanics(t, func() { 26 | process.New(fmt.Sprintf("stores.%s.Set", name), "key1", "foo").Run() 27 | process.New(fmt.Sprintf("stores.%s.Set", name), "key2", "bar").Run() 28 | process.New(fmt.Sprintf("stores.%s.Set", name), "key3", 1024).Run() 29 | process.New(fmt.Sprintf("stores.%s.Set", name), "key4", 0.618).Run() 30 | }) 31 | 32 | value = process.New(fmt.Sprintf("stores.%s.Get", name), "key1").Run() 33 | assert.Equal(t, "foo", value) 34 | 35 | value = process.New(fmt.Sprintf("stores.%s.GetDel", name), "key2").Run() 36 | assert.Equal(t, "bar", value) 37 | 38 | value = process.New(fmt.Sprintf("stores.%s.Has", name), "key2").Run() 39 | assert.False(t, value.(bool)) 40 | 41 | value = process.New(fmt.Sprintf("stores.%s.Len", name)).Run() 42 | assert.Equal(t, 3, value) 43 | 44 | value = process.New(fmt.Sprintf("stores.%s.Keys", name)).Run() 45 | 46 | assert.Contains(t, value, "key3") 47 | assert.Contains(t, value, "key4") 48 | assert.Contains(t, value, "key1") 49 | assert.NotContains(t, value, "key2") 50 | } 51 | -------------------------------------------------------------------------------- /store/redis/README.md: -------------------------------------------------------------------------------- 1 | # Redis Cache 2 | -------------------------------------------------------------------------------- /store/redis/types.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-redis/redis/v8" 7 | ) 8 | 9 | // Store redis store 10 | type Store struct { 11 | rdb *redis.Client 12 | Option Option 13 | } 14 | 15 | // Option redis option 16 | type Option struct { 17 | Timeout time.Duration 18 | Prefix string 19 | } 20 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/yaoapp/gou/application" 8 | "github.com/yaoapp/gou/connector" 9 | "github.com/yaoapp/gou/helper" 10 | "github.com/yaoapp/gou/store/lru" 11 | "github.com/yaoapp/gou/store/mongo" 12 | "github.com/yaoapp/gou/store/redis" 13 | "github.com/yaoapp/kun/exception" 14 | ) 15 | 16 | // Pools LRU pools 17 | var Pools = map[string]Store{} 18 | 19 | // Load load kv store 20 | func Load(file string, name string) (Store, error) { 21 | 22 | data, err := application.App.Read(file) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | inst := Instance{} 28 | err = application.Parse(file, data, &inst) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | typ := strings.ToLower(inst.Type) 34 | if typ == "lru" { 35 | stor, err := New(nil, inst.Option) 36 | if err != nil { 37 | return nil, err 38 | } 39 | Pools[name] = stor 40 | return Pools[name], nil 41 | } 42 | 43 | connector, has := connector.Connectors[inst.Connector] 44 | if !has { 45 | return nil, fmt.Errorf("Store %s Connector:%s was not loaded", name, inst.Connector) 46 | } 47 | 48 | stor, err := New(connector, inst.Option) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | Pools[name] = stor 54 | return Pools[name], nil 55 | } 56 | 57 | // Select Select loaded kv store 58 | func Select(name string) Store { 59 | store, has := Pools[name] 60 | if !has { 61 | exception.New("Store:%s does not load", 500, name).Throw() 62 | } 63 | return store 64 | } 65 | 66 | // New create a store via connector 67 | func New(c connector.Connector, option Option) (Store, error) { 68 | 69 | if c == nil { 70 | size := 10240 71 | if option != nil { 72 | if v, has := option["size"]; has { 73 | size = helper.EnvInt(v, 10240) 74 | } 75 | } 76 | return lru.New(size) 77 | } 78 | 79 | if c.Is(connector.REDIS) { 80 | return redis.New(c) 81 | } else if c.Is(connector.MONGO) { 82 | return mongo.New(c) 83 | } 84 | 85 | return nil, fmt.Errorf("the connector does not support") 86 | 87 | } 88 | -------------------------------------------------------------------------------- /store/store.types.go: -------------------------------------------------------------------------------- 1 | package store 2 | -------------------------------------------------------------------------------- /store/types.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "time" 4 | 5 | // Store The interface of a key-value store 6 | type Store interface { 7 | Get(key string) (value interface{}, ok bool) 8 | Set(key string, value interface{}, ttl time.Duration) error 9 | Del(key string) error 10 | Has(key string) bool 11 | Len() int 12 | Keys() []string 13 | Clear() 14 | GetSet(key string, ttl time.Duration, getValue func(key string) (interface{}, error)) (interface{}, error) 15 | GetDel(key string) (value interface{}, ok bool) 16 | GetMulti(keys []string) map[string]interface{} 17 | SetMulti(values map[string]interface{}, ttl time.Duration) 18 | DelMulti(keys []string) 19 | GetSetMulti(keys []string, ttl time.Duration, getValue func(key string) (interface{}, error)) map[string]interface{} 20 | } 21 | 22 | // Instance the kv-store setting 23 | type Instance struct { 24 | Name string `json:"name"` 25 | Description string `json:"description,omitempty"` 26 | Connector string `json:"connector,omitempty"` 27 | Type string `json:"type,omitempty"` // warning: type is deprecated in the future new version 28 | Option map[string]interface{} `json:"option,omitempty"` 29 | } 30 | 31 | // Option the store option 32 | type Option map[string]interface{} 33 | -------------------------------------------------------------------------------- /task/process_test.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "path/filepath" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/yaoapp/gou/application" 12 | "github.com/yaoapp/gou/process" 13 | v8 "github.com/yaoapp/gou/runtime/v8" 14 | "github.com/yaoapp/kun/utils" 15 | ) 16 | 17 | func TestLoadTask(t *testing.T) { 18 | taskLoad(t) 19 | assert.Equal(t, 1, len(Tasks)) 20 | assert.NotPanics(t, func() { Select("mail") }) 21 | } 22 | 23 | func TestTaskProcess(t *testing.T) { 24 | mail := taskLoad(t) 25 | go mail.Start() 26 | defer mail.Stop() 27 | id, err := process.New("tasks.mail.Add", "max@iqka.com").Exec() 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | id2, err := process.New("tasks.mail.Add", "max@iqka.com", 1).Exec() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | // time.Sleep(200 * time.Millisecond) 38 | s1, err := process.New("tasks.mail.Get", id).Exec() 39 | // if err != nil { 40 | // t.Fatal(err) 41 | // } 42 | 43 | time.Sleep(100 * time.Millisecond) 44 | s2, err := process.New("tasks.mail.Get", id).Exec() 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | // time.Sleep(100 * time.Millisecond) 50 | 51 | utils.Dump(s1, s2, id, id2) 52 | // assert.Equal(t, 1025, id) 53 | // assert.Equal(t, 1026, id2) 54 | // assert.Equal(t, "RUNNING", s1.(map[string]interface{})["status"]) 55 | // assert.Equal(t, "RUNNING", s2.(map[string]interface{})["status"]) 56 | // assert.Equal(t, 3, s2.(map[string]interface{})["total"]) 57 | // assert.Equal(t, "unit-test", s2.(map[string]interface{})["message"]) 58 | 59 | // waitting 60 | time.Sleep(3000 * time.Millisecond) 61 | } 62 | 63 | func taskLoad(t *testing.T) *Task { 64 | loadApp(t) 65 | loadScripts(t) 66 | 67 | process.Register("xiang.system.Sleep", func(process *process.Process) interface{} { 68 | process.ValidateArgNums(1) 69 | ms := process.ArgsInt(0) 70 | time.Sleep(time.Duration(ms) * time.Millisecond) 71 | return nil 72 | }) 73 | 74 | _, err := Load(path.Join("tasks", "mail.task.yao"), "mail") 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | return Select("mail") 80 | } 81 | 82 | func loadApp(t *testing.T) { 83 | 84 | root := os.Getenv("GOU_TEST_APPLICATION") 85 | app, err := application.OpenFromDisk(root) // Load app 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | application.Load(app) 90 | 91 | } 92 | 93 | func loadScripts(t *testing.T) { 94 | 95 | scripts := map[string]string{ 96 | "tests.task.mail": filepath.Join("scripts", "tests", "task", "mail.js"), 97 | } 98 | 99 | for id, file := range scripts { 100 | _, err := v8.Load(file, id) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | } 105 | 106 | err := v8.Start(&v8.Option{}) 107 | if err != nil { 108 | t.Fatal(err) 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /task/types.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | const ( 10 | // WAITING the job is waiting 11 | WAITING = iota + 1 12 | 13 | // RUNNING the job is running 14 | RUNNING 15 | 16 | // SUCCESS the job is success 17 | SUCCESS 18 | 19 | // FAILURE the job is failure 20 | FAILURE 21 | ) 22 | 23 | var status = map[int]string{ 24 | WAITING: "WAITING", 25 | RUNNING: "RUNNING", 26 | SUCCESS: "SUCCESS", 27 | FAILURE: "SUCCESS", 28 | } 29 | 30 | // Task the task struct 31 | type Task struct { 32 | name string 33 | timeout int 34 | handlers *Handlers 35 | pool *Pool 36 | jobs map[int]*Job 37 | mutex sync.Mutex 38 | ctx context.Context 39 | cancel context.CancelFunc 40 | Option Option 41 | } 42 | 43 | // Option the task option 44 | type Option struct { 45 | Name string 46 | JobQueueLength int 47 | WorkerNums int 48 | AttemptAfter int 49 | Attempts int 50 | Timeout int 51 | } 52 | 53 | // Pool the worker pool 54 | type Pool struct { 55 | size int 56 | max int 57 | jobque chan *Job 58 | workerque chan *Worker 59 | } 60 | 61 | // Worker the work struct 62 | type Worker struct { 63 | job chan *Job 64 | } 65 | 66 | // Job the job 67 | type Job struct { 68 | id int 69 | ctx context.Context 70 | cancel context.CancelFunc 71 | timeout time.Duration 72 | curr int 73 | total int 74 | status int 75 | message string 76 | response interface{} 77 | args []interface{} 78 | } 79 | 80 | // Handlers the event handlers 81 | type Handlers struct { 82 | Exec func(int, ...interface{}) (interface{}, error) 83 | Progress func(int, int, int, string) 84 | NextID func() (int, error) 85 | Add func(int) 86 | Success func(int, interface{}) 87 | Error func(int, error) 88 | } 89 | -------------------------------------------------------------------------------- /types/fs.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "hash/fnv" 6 | "net/textproto" 7 | "strings" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | // UploadFile upload file 13 | type UploadFile struct { 14 | UID string `json:"uid,omitempty"` // Content-Uid The unique identifier of the file ( for chunk upload ) 15 | Range string `json:"range,omitempty"` // Content-Range bytes start-end/total (for chunk upload) 16 | Sync bool `json:"sync,omitempty"` // Content-Sync sync upload or not. the default is false 17 | Name string `json:"name"` 18 | TempFile string `json:"tempFile"` 19 | Size int64 `json:"size"` 20 | Header textproto.MIMEHeader `json:"header"` 21 | Error string `json:"error"` 22 | } 23 | 24 | // UploadProgress upload progress 25 | type UploadProgress struct { 26 | Total int64 `json:"total"` 27 | Uploaded int64 `json:"uploaded"` 28 | Completed bool `json:"completed"` 29 | } 30 | 31 | // IsChunk is chunk upload or not 32 | func (upload UploadFile) IsChunk() bool { 33 | return upload.Range != "" 34 | } 35 | 36 | // IsError is error or not 37 | func (upload UploadFile) IsError() bool { 38 | return upload.Error != "" 39 | } 40 | 41 | // Hash hash 42 | func (upload UploadFile) Hash() string { 43 | 44 | h := fnv.New64a() 45 | // UUID Hash 46 | if upload.UID != "" { 47 | h.Write([]byte(fmt.Sprintf("%v", upload.UID))) 48 | return fmt.Sprintf("%x", h.Sum64()) 49 | } 50 | 51 | // TempFile Hash (for chunk upload) 52 | if upload.TempFile != "" { 53 | h.Write([]byte(fmt.Sprintf("%v", upload.TempFile))) 54 | return fmt.Sprintf("%x", h.Sum64()) 55 | } 56 | 57 | // General a uuid and hash 58 | uuid := uuid.NewString() 59 | h.Write([]byte(fmt.Sprintf("%v", uuid))) 60 | return fmt.Sprintf("%x", h.Sum64()) 61 | } 62 | 63 | // ChunkFileName get chunk file name 64 | func (upload UploadFile) ChunkFileName() string { 65 | name := strings.ReplaceAll(upload.Range, "bytes ", "") 66 | name = strings.ReplaceAll(name, "/", "_") 67 | return fmt.Sprintf("%s.chunk", name) 68 | } 69 | 70 | // TotalSize total size 71 | func (upload UploadFile) TotalSize() int64 { 72 | var total int64 73 | nameInfo := strings.Split(upload.Range, "/") 74 | fmt.Sscanf(nameInfo[len(nameInfo)-1], "%d", &total) 75 | return total 76 | } 77 | -------------------------------------------------------------------------------- /types/query.types.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // QueryParam 数据查询器参数 4 | type QueryParam struct { 5 | Model string `json:"model,omitempty"` 6 | Table string `json:"table,omitempty"` 7 | Alias string `json:"alias,omitempty"` 8 | Export string `json:"export,omitempty"` // 导出前缀 9 | Select []interface{} `json:"select,omitempty"` // string | dbal.Raw 10 | Wheres []QueryWhere `json:"wheres,omitempty"` 11 | Orders []QueryOrder `json:"orders,omitempty"` 12 | Limit int `json:"limit,omitempty"` 13 | Page int `json:"page,omitempty"` 14 | PageSize int `json:"pagesize,omitempty"` 15 | Withs map[string]With `json:"withs,omitempty"` 16 | } 17 | 18 | // With relations 关联查询 19 | type With struct { 20 | Name string `json:"name"` 21 | Query QueryParam `json:"query,omitempty"` 22 | } 23 | 24 | // QueryWhere Where 查询条件 25 | type QueryWhere struct { 26 | Rel string `json:"rel,omitempty"` // Relation Name 27 | Column interface{} `json:"column,omitempty"` 28 | Value interface{} `json:"value,omitempty"` 29 | Method string `json:"method,omitempty"` // where,orwhere, wherein, orwherein... 30 | OP string `json:"op,omitempty"` // 操作 eq/gt/lt/ge/le/like... 31 | Wheres []QueryWhere `json:"wheres,omitempty"` // 分组查询 32 | } 33 | 34 | // QueryOrder Order 查询排序 35 | type QueryOrder struct { 36 | Rel string `json:"rel,omitempty"` // Relation Name 37 | Column string `json:"column"` 38 | Option string `json:"option,omitempty"` // desc, asc 39 | } 40 | -------------------------------------------------------------------------------- /websocket/client_test.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestPush(t *testing.T) { 15 | srv, url := serve(t) 16 | defer srv.Stop() 17 | 18 | conn, err := NewWebSocket(url, []string{"po"}) 19 | if err != nil { 20 | t.Fatalf("%s", err) 21 | } 22 | 23 | err = Push(conn, "Hello World!") 24 | if err != nil { 25 | t.Fatalf("%s", err) 26 | } 27 | assert.Nil(t, err) 28 | } 29 | 30 | func TestOpen(t *testing.T) { 31 | srv, url := serve(t) 32 | defer srv.Stop() 33 | 34 | var ws *WSClient = nil 35 | var handlers = Handlers{ 36 | Connected: func(option WSClientOption) error { 37 | fmt.Println("onConnected", option) 38 | fmt.Println("Online", srv.Online()) 39 | fmt.Println("Clients", srv.Clients()) 40 | srv.Broadcast([]byte("Hello world")) 41 | time.Sleep(500 * time.Millisecond) 42 | ws.Write([]byte("1|I'm Here, The connection will be closed")) 43 | return nil 44 | }, 45 | Closed: func(data []byte, err error) []byte { 46 | fmt.Printf("onClosed: %s %v\n", data, err) 47 | return nil 48 | }, 49 | Data: func(data []byte, length int) ([]byte, error) { 50 | fmt.Printf("onData: %s %d\n", data, length) 51 | if data[0] == 0x31 { 52 | err := ws.Close() 53 | fmt.Println("Close connection", err) 54 | } 55 | return nil, nil 56 | }, 57 | Error: func(err error) { 58 | fmt.Printf("Error: %s\n", err) 59 | }, 60 | } 61 | ws = NewWSClient(WSClientOption{URL: url, Protocols: []string{"po"}}, handlers) 62 | err := ws.Open() 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | } 67 | 68 | func serve(t *testing.T) (*Upgrader, string) { 69 | 70 | ws, err := NewUpgrader("test") 71 | if err != nil { 72 | t.Fatalf("%s", err) 73 | } 74 | 75 | gin.SetMode(gin.ReleaseMode) 76 | router := gin.Default() 77 | ws.SetHandler(func(message []byte, client int) ([]byte, error) { return message, nil }) 78 | ws.SetRouter(router) 79 | 80 | listener, err := net.Listen("tcp", "127.0.0.1:0") 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | go ws.Start() 86 | go func() { 87 | http.Serve(listener, router) 88 | }() 89 | time.Sleep(200 * time.Millisecond) 90 | 91 | return ws, fmt.Sprintf("ws://127.0.0.1:%d/websocket/test", listener.Addr().(*net.TCPAddr).Port) 92 | } 93 | -------------------------------------------------------------------------------- /websocket/hub.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "encoding/binary" 5 | "sync" 6 | 7 | "github.com/gorilla/websocket" 8 | "github.com/yaoapp/kun/log" 9 | ) 10 | 11 | func newHub() *Hub { 12 | return &Hub{ 13 | broadcast: make(chan []byte), 14 | direct: make(chan []byte), 15 | register: make(chan *Client), 16 | unregister: make(chan *Client), 17 | clients: make(map[*Client]bool), 18 | interrupt: make(chan int), 19 | indexes: map[uint32]*Client{}, 20 | } 21 | } 22 | 23 | // NextID return the next client ID 24 | func (h *Hub) NextID() uint32 { 25 | var mutex sync.Mutex 26 | mutex.Lock() 27 | id := len(h.clients) 28 | mutex.Unlock() 29 | return uint32(id) + 1 30 | } 31 | 32 | // Clients return the online clients 33 | func (h *Hub) Clients() []uint32 { 34 | ids := []uint32{} 35 | for client := range h.clients { 36 | ids = append(ids, client.id) 37 | } 38 | return ids 39 | } 40 | 41 | // Nums count the online client's nums 42 | func (h *Hub) Nums() int { 43 | return len(h.clients) 44 | } 45 | 46 | // AddID add a id to the message 47 | // 0-4 id, 4~N message eg: [0 0 0 1 49 124...] 48 | func (h *Hub) AddID(id uint32, message []byte) []byte { 49 | head := make([]byte, 4) 50 | binary.BigEndian.PutUint32(head, id) 51 | res := append([]byte{}, head...) 52 | res = append(res, message...) 53 | return res 54 | } 55 | 56 | func (h *Hub) run() { 57 | LOOP: 58 | for { 59 | select { 60 | 61 | case client := <-h.register: 62 | h.clients[client] = true 63 | h.indexes[client.id] = client 64 | 65 | case client := <-h.unregister: 66 | if _, ok := h.clients[client]; ok { 67 | delete(h.indexes, client.id) 68 | delete(h.clients, client) 69 | close(client.send) 70 | } 71 | 72 | case message := <-h.direct: 73 | if len(message) > 4 { 74 | // 0-4 id, 4~N message eg: [0 0 0 1 49 124...] 75 | id := binary.BigEndian.Uint32(message[0:4]) 76 | msg := message[4:] 77 | if client, ok := h.indexes[id]; ok { 78 | select { 79 | case client.send <- msg: 80 | default: 81 | close(client.send) 82 | delete(h.indexes, client.id) 83 | delete(h.clients, client) 84 | } 85 | } 86 | } 87 | 88 | case message := <-h.broadcast: 89 | for client := range h.clients { 90 | select { 91 | case client.send <- message: 92 | default: 93 | close(client.send) 94 | delete(h.indexes, client.id) 95 | delete(h.clients, client) 96 | } 97 | } 98 | 99 | case exit := <-h.interrupt: 100 | if exit == 1 { 101 | for client := range h.clients { 102 | err := client.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseServiceRestart, "Repair")) 103 | log.Trace("Close Client Connection, %v", err) 104 | // close(client.send) 105 | // delete(h.clients, client) 106 | } 107 | break LOOP 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /websocket/upgrader_test.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | // var cstDialer = websocket.Dialer{ 4 | // Subprotocols: []string{"p1", "p2"}, 5 | // ReadBufferSize: 1024, 6 | // WriteBufferSize: 1024, 7 | // HandshakeTimeout: 30 * time.Second, 8 | // } 9 | 10 | // func TestStart(t *testing.T) { 11 | // log.SetOutput(os.Stdout) 12 | // log.SetLevel(log.TraceLevel) 13 | // ws := &WebSocket{} 14 | 15 | // http.HandleFunc("/echo", func(rw http.ResponseWriter, r *http.Request) { 16 | // ws.Start(rw, r, nil) 17 | // }) 18 | 19 | // go func() { http.ListenAndServe("127.0.0.1:5081", nil) }() 20 | 21 | // for i := 0; i < 3; i++ { 22 | // time.Sleep(200 * time.Microsecond) 23 | // conn, err := Dial() 24 | // if err != nil || conn == nil { 25 | // continue 26 | // } 27 | // echo(t, conn) 28 | // break 29 | // } 30 | // } 31 | 32 | // func Dial() (*websocket.Conn, error) { 33 | // ws, _, err := cstDialer.Dial("ws://127.0.0.1:5081/echo", nil) 34 | // if err != nil { 35 | // log.Error("Dial: %v", err) 36 | // } 37 | // return ws, nil 38 | // } 39 | 40 | // func echo(t *testing.T, ws *websocket.Conn) { 41 | 42 | // const message = "Hello World!" 43 | // if err := ws.SetWriteDeadline(time.Now().Add(time.Second)); err != nil { 44 | // t.Fatalf("SetWriteDeadline: %v", err) 45 | // } 46 | // if err := ws.WriteMessage(websocket.TextMessage, []byte(message)); err != nil { 47 | // t.Fatalf("WriteMessage: %v", err) 48 | // } 49 | // if err := ws.SetReadDeadline(time.Now().Add(time.Second)); err != nil { 50 | // t.Fatalf("SetReadDeadline: %v", err) 51 | // } 52 | // _, p, err := ws.ReadMessage() 53 | // if err != nil { 54 | // t.Fatalf("ReadMessage: %v", err) 55 | // } 56 | // if string(p) != message { 57 | // t.Fatalf("message=%s, want %s", p, message) 58 | // } 59 | 60 | // log.Trace("Message:%s", message) 61 | // } 62 | --------------------------------------------------------------------------------