├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cache.go ├── call.go ├── call_test.go ├── cmd └── starlet │ ├── .gitignore │ ├── Makefile │ ├── arg.star │ ├── cgi.star │ ├── config.go │ ├── go.mod │ ├── go.sum │ ├── main.go │ └── mod.go ├── common_test.go ├── config.go ├── dataconv ├── helper.go ├── helper_test.go ├── interface.go ├── marshal.go ├── marshal_test.go ├── share.go ├── share_test.go └── types │ ├── either.go │ ├── either_test.go │ ├── hybrid.go │ ├── hybrid_test.go │ ├── many.go │ ├── many_test.go │ ├── nullable.go │ └── nullable_test.go ├── error.go ├── exec.go ├── exec_test.go ├── go.mod ├── go.sum ├── internal ├── doc.go ├── hash.go ├── hash_test.go ├── replacecr │ ├── replace_cr.go │ └── replace_cr_test.go ├── testloader.go └── testloader_test.go ├── internal_test.go ├── lib ├── atom │ ├── README.md │ ├── atom.go │ ├── atom_test.go │ ├── helper.go │ ├── helper_test.go │ └── method.go ├── base64 │ ├── README.md │ ├── base64.go │ └── base64_test.go ├── csv │ ├── README.md │ ├── csv.go │ └── csv_test.go ├── file │ ├── README.md │ ├── byte.go │ ├── copy.go │ ├── copy_test.go │ ├── file.go │ ├── file_test.go │ ├── json.go │ ├── line.go │ ├── stat.go │ └── testdata │ │ ├── .gitattributes │ │ ├── 1line.txt │ │ ├── 1line_nl.txt │ │ ├── aloha.txt │ │ ├── bom.txt │ │ ├── empty.txt │ │ ├── json1.json │ │ ├── json2.json │ │ ├── line_mac.txt │ │ ├── line_win.txt │ │ └── noext ├── goidiomatic │ ├── README.md │ ├── idiomatic.go │ └── idiomatic_test.go ├── hashlib │ ├── README.md │ ├── hash.go │ └── hash_test.go ├── http │ ├── README.md │ ├── http.go │ ├── http_test.go │ ├── internal_test.go │ ├── server.go │ └── server_test.go ├── json │ ├── README.md │ ├── json.go │ └── json_test.go ├── log │ ├── README.md │ ├── zaplog.go │ └── zaplog_test.go ├── net │ ├── network.go │ ├── network_test.go │ ├── ping.go │ └── ping_test.go ├── path │ ├── README.md │ ├── path.go │ └── path_test.go ├── random │ ├── README.md │ ├── random.go │ └── random_test.go ├── re │ ├── README.md │ ├── re.go │ └── re_test.go ├── runtime │ ├── README.md │ ├── runtime.go │ └── runtime_test.go ├── stats │ ├── README.md │ ├── stats.go │ └── stats_test.go └── string │ ├── README.md │ ├── string.go │ └── string_test.go ├── machine.go ├── machine_test.go ├── module.go ├── module_test.go ├── run.go ├── run_test.go ├── testdata ├── aloha.star ├── calc.star ├── circle1.star ├── circle2.star ├── coins.star ├── empty.star ├── factorial.star ├── fibonacci.star ├── fibonacci2.star ├── magic.star ├── nemo │ └── two.star ├── one.star └── two.star ├── types.go └── types_test.go /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | 9 | defaults: 10 | run: 11 | shell: bash 12 | 13 | permissions: read-all 14 | 15 | jobs: 16 | build: 17 | name: Test with ${{ matrix.go-version }} on ${{ matrix.vm-os }} 18 | runs-on: ${{ matrix.vm-os }} 19 | env: 20 | CI_REPORT: ${{ matrix.vm-os == 'ubuntu-20.04' && startsWith(matrix.go-version, '1.18.') }} 21 | strategy: 22 | max-parallel: 10 23 | fail-fast: false 24 | matrix: 25 | vm-os: [ 26 | ubuntu-20.04, 27 | macos-13, 28 | macos-14, 29 | windows-2022 30 | ] 31 | go-version: [ 32 | 1.18.x, 33 | 1.19.x, 34 | 1.20.x, 35 | 1.21.x, 36 | 1.22.x, 37 | 1.23.x, 38 | ] 39 | permissions: 40 | contents: read 41 | # Steps to execute 42 | steps: 43 | - uses: actions/checkout@v3 44 | - name: Set up Go 45 | uses: actions/setup-go@v3 46 | with: 47 | go-version: ${{ matrix.go-version }} 48 | cache: true 49 | - name: Go Build 50 | run: | 51 | export 52 | git status 53 | go version 54 | go mod download 55 | make --version 56 | - name: Test 57 | run: | 58 | make ci 59 | make build 60 | - name: Upload Coverage Reports to Codecov 61 | if: ${{ fromJSON(env.CI_REPORT) }} 62 | uses: codecov/codecov-action@v3 63 | with: 64 | token: ${{ secrets.CODECOV_TOKEN }} 65 | files: coverage.txt 66 | - name: Upload Coverage Reports to Codacy 67 | if: ${{ fromJSON(env.CI_REPORT) }} 68 | env: 69 | CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} 70 | run: 71 | bash <(curl -Ls https://coverage.codacy.com/get.sh) report --force-coverage-parser go -r coverage.txt 72 | - name: Analyze 73 | if: ${{ runner.os == 'macOS' || runner.os == 'Linux' }} 74 | run: | 75 | # Setup 76 | if [[ ${{ runner.os }} == 'Linux' ]]; then 77 | wget -cqL https://github.com/XAMPPRocky/tokei/releases/download/v12.1.2/tokei-i686-unknown-linux-musl.tar.gz -O tokei.tgz 78 | wget -cqL https://github.com/mgechev/revive/releases/download/v1.3.7/revive_linux_amd64.tar.gz -O revive.tgz 79 | elif [[ ${{ runner.os }} == 'macOS' ]]; then 80 | wget -cqL https://github.com/XAMPPRocky/tokei/releases/download/v12.1.2/tokei-x86_64-apple-darwin.tar.gz -O tokei.tgz 81 | wget -cqL https://github.com/mgechev/revive/releases/download/v1.3.7/revive_darwin_amd64.tar.gz -O revive.tgz 82 | fi 83 | tar zxf tokei.tgz tokei && chmod +x tokei && $SUDO mv tokei /usr/local/bin && rm tokei.tgz 84 | tar zxf revive.tgz revive && chmod +x revive && $SUDO mv revive /usr/local/bin && rm revive.tgz 85 | wget -cqL https://raw.githubusercontent.com/1set/meta/master/revive.toml -O revive.toml 86 | # Analyze 87 | echo "# Analysis on ${{ runner.os }}" > $GITHUB_STEP_SUMMARY 88 | uname -a >> $GITHUB_STEP_SUMMARY 89 | # --- count lines of code 90 | echo "## Tokei Result" >> $GITHUB_STEP_SUMMARY 91 | printf '\n```\n' >> $GITHUB_STEP_SUMMARY 92 | tokei >> $GITHUB_STEP_SUMMARY 93 | printf '```\n\n' >> $GITHUB_STEP_SUMMARY 94 | # --- lint 95 | echo "## Revive Result" >> $GITHUB_STEP_SUMMARY 96 | printf '\n```\n' >> $GITHUB_STEP_SUMMARY 97 | revive -config revive.toml -formatter friendly ./... >> $GITHUB_STEP_SUMMARY 98 | printf '```\n\n' >> $GITHUB_STEP_SUMMARY 99 | # --- file size 100 | echo "## File Size" >> $GITHUB_STEP_SUMMARY 101 | printf '\n```bash\n' >> $GITHUB_STEP_SUMMARY 102 | export CMDDIR=cmd/starlet 103 | ls -laSh "$CMDDIR" >> $GITHUB_STEP_SUMMARY 104 | printf '```\n\n```bash\n' >> $GITHUB_STEP_SUMMARY 105 | if [[ ${{ runner.os }} == 'Linux' ]]; then 106 | find "$CMDDIR" -maxdepth 1 -type f -size +524288c | xargs -I {} stat --format="%n %s" {} | awk '{printf "%s\t\t%sB\n", $1, $2}' >> $GITHUB_STEP_SUMMARY 107 | elif [[ ${{ runner.os }} == 'macOS' ]]; then 108 | find "$CMDDIR" -maxdepth 1 -type f -size +524288c | xargs -I {} stat -f "%N %z" {} | awk '{printf "%s\t\t%sB\n", $1, $2}' >> $GITHUB_STEP_SUMMARY 109 | fi 110 | printf '```\n\n' >> $GITHUB_STEP_SUMMARY 111 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | # we don't check in vendor, the lock file is just there so others know what we've tested against. 17 | vendor 18 | 19 | # VScode 20 | .vscode 21 | 22 | # stupid osx 23 | .DS_Store 24 | 25 | # Projects 26 | *.log 27 | coverage.txt 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 A Set of Useful Libraries and Utilities 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS := --print-directory 2 | SHELL := bash 3 | .ONESHELL: 4 | .SHELLFLAGS := -eu -o pipefail -c 5 | 6 | BINARY=starlet 7 | 8 | # for CircleCI, GitHub Actions, GitLab CI build number 9 | ifeq ($(origin CIRCLE_BUILD_NUM), environment) 10 | BUILD_NUM ?= cc$(CIRCLE_BUILD_NUM) 11 | else ifeq ($(origin GITHUB_RUN_NUMBER), environment) 12 | BUILD_NUM ?= gh$(GITHUB_RUN_NUMBER) 13 | else ifeq ($(origin CI_PIPELINE_IID), environment) 14 | BUILD_NUM ?= gl$(CI_PIPELINE_IID) 15 | endif 16 | 17 | # for go dev 18 | GOCMD=go 19 | GORUN=$(GOCMD) run 20 | GOBUILD=$(GOCMD) build 21 | GOTEST=$(GOCMD) test 22 | GODOC=$(GOCMD) doc 23 | GOGET=$(GOCMD) get 24 | GOMOD=$(GOCMD) mod 25 | 26 | # for go build 27 | # export CGO_ENABLED=0 28 | export TZ=Asia/Shanghai 29 | export PACK=main 30 | export FLAGS="-s -w -X '$(PACK).AppName=$(BINARY)' -X '$(PACK).BuildDate=`date '+%Y-%m-%dT%T%z'`' -X '$(PACK).BuildHost=`hostname`' -X '$(PACK).GoVersion=`go version`' -X '$(PACK).GitBranch=`git symbolic-ref -q --short HEAD`' -X '$(PACK).GitCommit=`git rev-parse --short HEAD`' -X '$(PACK).GitSummary=`git describe --tags --dirty --always`' -X '$(PACK).CIBuildNum=${BUILD_NUM}'" 31 | 32 | # commands 33 | .PHONY: default ci test test_loop bench build 34 | default: 35 | @echo "build target is required for $(BINARY)" 36 | @exit 1 37 | 38 | ci: 39 | $(GOTEST) -v -race -cover -covermode=atomic -coverprofile=coverage.txt -count 1 ./... 40 | $(GOTEST) -v -parallel=4 -run="none" -benchtime="2s" -benchmem -bench=. 41 | make -C cmd/starlet test 42 | 43 | build: 44 | make -C cmd/starlet build 45 | 46 | test: 47 | $(GOTEST) -v -race -cover -covermode=atomic -count 1 ./... 48 | 49 | test_loop: 50 | while true; do \ 51 | $(GOTEST) -v -race -cover -covermode=atomic -count 1 ./...; \ 52 | if [[ $$? -ne 0 ]]; then \ 53 | break; \ 54 | fi; \ 55 | done 56 | 57 | bench: 58 | $(GOTEST) -parallel=4 -run="none" -benchtime="2s" -benchmem -bench=./... 59 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package starlet 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | "sync/atomic" 8 | "unsafe" 9 | 10 | "go.starlark.net/starlark" 11 | "go.starlark.net/syntax" 12 | ) 13 | 14 | // The following code is copied and modified from the starlark-go repo, 15 | // https://go.starlark.net/starlark and is Copyright 2017 the Bazel authors, 16 | // with a BSD 3-clause license (see the LICENSE file in that repo). 17 | // Original source code: https://github.com/google/starlark-go/blob/master/starlark/example_test.go#L211 18 | 19 | // cache is a concurrency-safe, duplicate-suppressing, 20 | // non-blocking cache of the doLoad function. 21 | // See Section 9.7 of gopl.io for an explanation of this structure. 22 | // It also features online deadlock (load cycle) detection. 23 | type cache struct { 24 | cacheMu sync.Mutex 25 | cache map[string]*entry 26 | globals starlark.StringDict 27 | execOpts *syntax.FileOptions 28 | loadMod func(s string) (starlark.StringDict, error) // load from built-in module first 29 | readFile func(s string) ([]byte, error) // and then from file system 30 | } 31 | 32 | type entry struct { 33 | owner unsafe.Pointer // a *cycleChecker; see cycleCheck 34 | globals starlark.StringDict 35 | err error 36 | ready chan struct{} 37 | } 38 | 39 | func (c *cache) Load(module string) (starlark.StringDict, error) { 40 | return c.get(new(cycleChecker), module) 41 | } 42 | 43 | func (c *cache) remove(module string) { 44 | c.cacheMu.Lock() 45 | delete(c.cache, module) 46 | c.cacheMu.Unlock() 47 | } 48 | 49 | func (c *cache) reset() { 50 | c.cacheMu.Lock() 51 | c.cache = make(map[string]*entry) 52 | c.cacheMu.Unlock() 53 | } 54 | 55 | // get loads and returns an entry (if not already loaded). 56 | func (c *cache) get(cc *cycleChecker, module string) (starlark.StringDict, error) { 57 | c.cacheMu.Lock() 58 | e := c.cache[module] 59 | if e != nil { 60 | c.cacheMu.Unlock() 61 | // Some other goroutine is getting this module. 62 | // Wait for it to become ready. 63 | 64 | // Detect load cycles to avoid deadlocks. 65 | if err := cycleCheck(e, cc); err != nil { 66 | return nil, err 67 | } 68 | 69 | cc.setWaitsFor(e) 70 | <-e.ready 71 | cc.setWaitsFor(nil) 72 | } else { 73 | // First request for this module. 74 | e = &entry{ready: make(chan struct{})} 75 | c.cache[module] = e 76 | c.cacheMu.Unlock() 77 | 78 | e.setOwner(cc) 79 | e.globals, e.err = c.doLoad(cc, module) 80 | e.setOwner(nil) 81 | 82 | // Broadcast that the entry is now ready. 83 | close(e.ready) 84 | } 85 | return e.globals, e.err 86 | } 87 | 88 | func (c *cache) doLoad(cc *cycleChecker, module string) (starlark.StringDict, error) { 89 | thread := &starlark.Thread{ 90 | Print: func(_ *starlark.Thread, msg string) { fmt.Println(msg) }, 91 | Load: func(_ *starlark.Thread, module string) (starlark.StringDict, error) { 92 | // Tunnel the cycle-checker state for this "thread of loading". 93 | return c.get(cc, module) 94 | }, 95 | } 96 | 97 | // 1: load from built-in module, the first field returns nil if not found 98 | m, err := c.loadMod(module) 99 | if err != nil { 100 | // fail to load module 101 | return nil, err 102 | } 103 | if m != nil { 104 | // module found and loaded 105 | return m, nil 106 | } 107 | 108 | // 2: load from source file 109 | b, err := c.readFile(module) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | // 3. execute the source file 115 | if c.execOpts == nil { 116 | return starlark.ExecFile(thread, module, b, c.globals) 117 | } 118 | return starlark.ExecFileOptions(c.execOpts, thread, module, b, c.globals) 119 | } 120 | 121 | // -- concurrent cycle checking -- 122 | 123 | // A cycleChecker is used for concurrent deadlock detection. 124 | // Each top-level call to Load creates its own cycleChecker, 125 | // which is passed to all recursive calls it makes. 126 | // It corresponds to a logical thread in the deadlock detection literature. 127 | type cycleChecker struct { 128 | waitsFor unsafe.Pointer // an *entry; see cycleCheck 129 | } 130 | 131 | func (cc *cycleChecker) setWaitsFor(e *entry) { 132 | atomic.StorePointer(&cc.waitsFor, unsafe.Pointer(e)) 133 | } 134 | 135 | func (e *entry) setOwner(cc *cycleChecker) { 136 | atomic.StorePointer(&e.owner, unsafe.Pointer(cc)) 137 | } 138 | 139 | // cycleCheck reports whether there is a path in the waits-for graph 140 | // from resource 'e' to thread 'me'. 141 | // 142 | // The waits-for graph (WFG) is a bipartite graph whose nodes are 143 | // alternately of type entry and cycleChecker. Each node has at most 144 | // one outgoing edge. An entry has an "owner" edge to a cycleChecker 145 | // while it is being readied by that cycleChecker, and a cycleChecker 146 | // has a "waits-for" edge to an entry while it is waiting for that entry 147 | // to become ready. 148 | // 149 | // Before adding a waits-for edge, the cache checks whether the new edge 150 | // would form a cycle. If so, this indicates that the load graph is 151 | // cyclic and that the following wait operation would deadlock. 152 | func cycleCheck(e *entry, me *cycleChecker) error { 153 | for e != nil { 154 | cc := (*cycleChecker)(atomic.LoadPointer(&e.owner)) 155 | if cc == nil { 156 | break 157 | } 158 | if cc == me { 159 | return errors.New("cycle in load graph") 160 | } 161 | e = (*entry)(atomic.LoadPointer(&cc.waitsFor)) 162 | } 163 | return nil 164 | } 165 | -------------------------------------------------------------------------------- /call.go: -------------------------------------------------------------------------------- 1 | package starlet 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/1set/starlight/convert" 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | // Call executes a Starlark function or builtin saved in the thread and returns the result. 11 | func (m *Machine) Call(name string, args ...interface{}) (out interface{}, err error) { 12 | m.mu.Lock() 13 | defer m.mu.Unlock() 14 | 15 | defer func() { 16 | if r := recover(); r != nil { 17 | err = errorStarlarkPanic("call", r) 18 | } 19 | }() 20 | 21 | // preconditions 22 | if name == "" { 23 | return nil, errorStarletErrorf("call", "no function name") 24 | } 25 | if m.predeclared == nil || m.thread == nil { 26 | return nil, errorStarletErrorf("call", "no function loaded") 27 | } 28 | var callFunc starlark.Callable 29 | if rf, ok := m.predeclared[name]; !ok { 30 | return nil, errorStarletErrorf("call", "no such function: %s", name) 31 | } else if sf, ok := rf.(*starlark.Function); ok { 32 | callFunc = sf 33 | } else if sb, ok := rf.(*starlark.Builtin); ok { 34 | callFunc = sb 35 | } else { 36 | return nil, errorStarletErrorf("call", "mistyped function: %s", name) 37 | } 38 | 39 | // convert arguments 40 | sl := starlark.Tuple{} 41 | for _, arg := range args { 42 | sv, err := convert.ToValueWithTag(arg, m.customTag) 43 | if err != nil { 44 | return nil, errorStarlightConvert("args", err) 45 | } 46 | sl = append(sl, sv) 47 | } 48 | 49 | // reset thread 50 | m.thread.Uncancel() 51 | m.thread.SetLocal("context", context.TODO()) 52 | 53 | // call and convert result 54 | res, err := starlark.Call(m.thread, callFunc, sl, nil) 55 | if m.enableOutConv { // convert to interface{} if enabled 56 | out = convert.FromValue(res) 57 | } else { 58 | out = res 59 | } 60 | // handle error 61 | if err != nil { 62 | return out, errorStarlarkError("call", err) 63 | } 64 | return out, nil 65 | } 66 | -------------------------------------------------------------------------------- /call_test.go: -------------------------------------------------------------------------------- 1 | package starlet_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/1set/starlet" 10 | "go.starlark.net/starlark" 11 | ) 12 | 13 | func TestMachine_Call_Preconditions(t *testing.T) { 14 | m := starlet.NewDefault() 15 | 16 | // test: if name == "" 17 | _, err := m.Call("") 18 | expectErr(t, err, "starlet: call: no function name") 19 | 20 | // test: if m.thread == nil 21 | _, err = m.Call("no_thread") 22 | expectErr(t, err, "starlet: call: no function loaded") 23 | 24 | // test: if m.predeclared == nil 25 | m.SetGlobals(map[string]interface{}{"x": 1}) 26 | _, err = m.Call("no_globals") 27 | expectErr(t, err, "starlet: call: no function loaded") 28 | 29 | // prepare: run a script to load a function if exists 30 | _, err = m.RunScript([]byte(`y = 2`), map[string]interface{}{ 31 | "println": fmt.Println, 32 | }) 33 | if err != nil { 34 | t.Errorf("expected no error, got %v", err) 35 | } 36 | 37 | // test: if no such function 38 | _, err = m.Call("no_such_function") 39 | expectErr(t, err, "starlet: call: no such function: no_such_function") 40 | 41 | // test: if mistyped function 42 | _, err = m.Call("y") 43 | expectErr(t, err, "starlet: call: mistyped function: y") 44 | 45 | ei := err.(starlet.ExecError).Unwrap() 46 | expectErr(t, ei, "mistyped function: y") 47 | 48 | // test: if builtin function 49 | _, err = m.Call("println", "hello") 50 | if err != nil { 51 | t.Errorf("expected no error, got %v", err) 52 | } 53 | } 54 | 55 | func TestMachine_Call_Functions(t *testing.T) { 56 | type mockData struct { 57 | Apple string `json:"app"` 58 | Banana int64 `json:"bana"` 59 | Coconut bool `json:"coco"` 60 | } 61 | tests := []struct { 62 | name string 63 | code string 64 | args []interface{} 65 | want interface{} 66 | wantErr string 67 | }{ 68 | { 69 | name: "no args nor return", 70 | code: ` 71 | def work(): 72 | pass 73 | `, 74 | want: nil, 75 | }, 76 | { 77 | name: "no args but return", 78 | code: ` 79 | def work(): 80 | return 1 81 | `, 82 | want: int64(1), 83 | }, 84 | { 85 | name: "args but no return", 86 | code: ` 87 | def work(x, y): 88 | pass 89 | `, 90 | args: []interface{}{1, 2}, 91 | want: nil, 92 | }, 93 | { 94 | name: "args and return", 95 | code: ` 96 | def work(x, y): 97 | return x + y 98 | `, 99 | args: []interface{}{1, 2}, 100 | want: int64(3), 101 | }, 102 | { 103 | name: "lambda", 104 | code: ` 105 | work = lambda x, y: x * y 106 | `, 107 | args: []interface{}{2, 3}, 108 | want: int64(6), 109 | }, 110 | { 111 | name: "multiple return", 112 | code: ` 113 | def work(x, y): 114 | return x + 1, y + 2 115 | `, 116 | args: []interface{}{1, 2}, 117 | want: []interface{}{int64(2), int64(4)}, 118 | }, 119 | { 120 | name: "multiple return with tuple", 121 | code: ` 122 | def work(x, y): 123 | return (x + 1, y + 2) 124 | `, 125 | args: []interface{}{1, 2}, 126 | want: []interface{}{int64(2), int64(4)}, 127 | }, 128 | { 129 | name: "multiple return with list", 130 | code: ` 131 | def work(x, y): 132 | return [x + 1, y + 2] 133 | `, 134 | args: []interface{}{1, 2}, 135 | want: []interface{}{int64(2), int64(4)}, 136 | }, 137 | { 138 | name: "convert args fail", 139 | code: ` 140 | def work(x, y): 141 | return x + y 142 | `, 143 | args: []interface{}{1, make(chan int64)}, 144 | wantErr: `starlight: convert args: type chan int64 is not a supported starlark type`, 145 | }, 146 | { 147 | name: "invalid args", 148 | code: ` 149 | def work(x, y): 150 | return x + y 151 | `, 152 | args: []interface{}{1, "two"}, 153 | wantErr: `starlark: call: unknown binary op: int + string`, 154 | }, 155 | { 156 | name: "func runtime error", 157 | code: ` 158 | def work(x, y): 159 | fail("oops") 160 | `, 161 | args: []interface{}{1, 2}, 162 | wantErr: `starlark: call: fail: oops`, 163 | }, 164 | { 165 | name: "func runtime panic", 166 | code: ` 167 | def work(x, y): 168 | panic("outside starlark") 169 | `, 170 | args: []interface{}{1, 2}, 171 | wantErr: `starlark: call: panic: as expected`, 172 | }, 173 | { 174 | name: "custom tag for arg", 175 | args: []interface{}{ 176 | mockData{Apple: "red", Banana: 2, Coconut: true}, 177 | }, 178 | code: ` 179 | def work(x): 180 | return "apple is " + x.app + ", banana is " + str(x.bana) + ", coconut is " + str(x.coco) 181 | `, 182 | want: "apple is red, banana is 2, coconut is True", 183 | }, 184 | } 185 | 186 | for _, tt := range tests { 187 | t.Run(tt.name, func(t *testing.T) { 188 | // prepare to load 189 | m := starlet.NewDefault() 190 | m.SetCustomTag("json") 191 | _, err := m.RunScript([]byte(tt.code), map[string]interface{}{ 192 | "panic": starlark.NewBuiltin("panic", func(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 193 | panic(errors.New("as expected")) 194 | }), 195 | }) 196 | if err != nil { 197 | t.Errorf("expected no error, got %v", err) 198 | return 199 | } 200 | 201 | // call and check 202 | got, err := m.Call("work", tt.args...) 203 | if err != nil { 204 | if tt.wantErr == "" { 205 | t.Errorf("expected no error, got %v", err) 206 | } else { 207 | expectErr(t, err, tt.wantErr) 208 | } 209 | } else if !reflect.DeepEqual(got, tt.want) { 210 | t.Errorf("expected %v (%T), got %v (%T)", tt.want, tt.want, got, got) 211 | } 212 | }) 213 | } 214 | } 215 | 216 | func TestMachine_Call_Convert(t *testing.T) { 217 | m := starlet.NewDefault() 218 | m.SetCustomTag("json") 219 | _, err := m.RunScript([]byte(` 220 | def work(x, y): 221 | return x * y 222 | `), nil) 223 | if err != nil { 224 | t.Errorf("expected no error, got %v", err) 225 | return 226 | } 227 | 228 | // call and check --- conversion is enabled by default 229 | got, err := m.Call("work", 10, 20) 230 | if err != nil { 231 | t.Errorf("expected no error, got %v", err) 232 | return 233 | } 234 | if got != int64(200) { 235 | t.Errorf("expected 200, got %v", got) 236 | } 237 | 238 | // not convert 239 | m.SetOutputConversionEnabled(false) 240 | got, err = m.Call("work", 5, 6) 241 | if err != nil { 242 | t.Errorf("expected no error, got %v", err) 243 | return 244 | } 245 | if v, ok := got.(starlark.Value); !ok { 246 | t.Errorf("expected starlark Value, got %T", got) 247 | return 248 | } else if v != starlark.MakeInt(30) { 249 | t.Errorf("got unexpected value: %v", v) 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /cmd/starlet/.gitignore: -------------------------------------------------------------------------------- 1 | /starlet* 2 | -------------------------------------------------------------------------------- /cmd/starlet/Makefile: -------------------------------------------------------------------------------- 1 | MAKEFLAGS := --print-directory 2 | SHELL := bash 3 | .ONESHELL: 4 | .SHELLFLAGS := -eu -o pipefail -c 5 | 6 | BINARY=starlet 7 | 8 | # for CircleCI, GitHub Actions, GitLab CI build number 9 | ifeq ($(origin CIRCLE_BUILD_NUM), environment) 10 | BUILD_NUM ?= cc$(CIRCLE_BUILD_NUM) 11 | else ifeq ($(origin GITHUB_RUN_NUMBER), environment) 12 | BUILD_NUM ?= gh$(GITHUB_RUN_NUMBER) 13 | else ifeq ($(origin CI_PIPELINE_IID), environment) 14 | BUILD_NUM ?= gl$(CI_PIPELINE_IID) 15 | endif 16 | 17 | # for go dev 18 | GOCMD=go 19 | GORUN=$(GOCMD) run 20 | GOBUILD=$(GOCMD) build 21 | GOTEST=$(GOCMD) test 22 | GODOC=$(GOCMD) doc 23 | GOGET=$(GOCMD) get 24 | GOMOD=$(GOCMD) mod 25 | 26 | # for go build 27 | export CGO_ENABLED=0 28 | export TZ=Asia/Shanghai 29 | export PACK=main 30 | export FLAGS="-s -w -X '$(PACK).AppName=$(BINARY)' -X '$(PACK).BuildDate=`date '+%Y-%m-%dT%T%z'`' -X '$(PACK).BuildHost=`hostname`' -X '$(PACK).GoVersion=`go version`' -X '$(PACK).GitBranch=`git symbolic-ref -q --short HEAD`' -X '$(PACK).GitCommit=`git rev-parse --short HEAD`' -X '$(PACK).GitSummary=`git describe --tags --dirty --always`' -X '$(PACK).CIBuildNum=${BUILD_NUM}'" 31 | 32 | # commands 33 | .PHONY: default build build_linux build_mac build_windows run install 34 | default: 35 | @echo "build target is required for $(BINARY)" 36 | @exit 1 37 | 38 | build: 39 | $(GOBUILD) -v -ldflags $(FLAGS) -trimpath -o $(BINARY) . 40 | 41 | build_linux: 42 | GOOS=linux GOARCH=amd64 $(GOBUILD) -v -ldflags $(FLAGS) -trimpath -o $(BINARY) . 43 | 44 | build_mac: 45 | GOOS=darwin GOARCH=amd64 $(GOBUILD) -v -ldflags $(FLAGS) -trimpath -o $(BINARY) . 46 | 47 | build_windows: 48 | GOOS=windows GOARCH=amd64 $(GOBUILD) -v -ldflags $(FLAGS) -trimpath -o $(BINARY).exe . 49 | 50 | run: build 51 | ./$(BINARY) 52 | test: build 53 | ./$(BINARY) arg.star Aloha 54 | 55 | install: build 56 | ifndef GOBIN 57 | $(error GOBIN is not set) 58 | endif 59 | @if [ ! -d "$(GOBIN)" ]; then echo "Directory $(GOBIN) does not exist"; exit 1; fi 60 | cp $(BINARY) $(GOBIN) 61 | -------------------------------------------------------------------------------- /cmd/starlet/arg.star: -------------------------------------------------------------------------------- 1 | #import sys 2 | 3 | print("cnt:", len(sys.argv)) 4 | print("argv:", sys.argv) 5 | print("platform:", sys.platform) 6 | print("cwd:", path.getcwd()) 7 | print("path:", runtime.getenv("PATH")) -------------------------------------------------------------------------------- /cmd/starlet/cgi.star: -------------------------------------------------------------------------------- 1 | now = time.now() 2 | text = ''' 3 | 4 | 5 | 6 | My Homepage 7 | 8 | 9 |

Welcome to my homepage!

10 |

Current time is {}.

11 |
Your header: {}
12 |

This is a simple CGI script written in Starlark.

13 | 14 | 15 | '''.format(now, json.dumps(request.header, indent=2)).strip() 16 | 17 | response.set_html(text) 18 | -------------------------------------------------------------------------------- /cmd/starlet/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | cl "bitbucket.org/ai69/colorlogo" 8 | "github.com/1set/gut/yos" 9 | "github.com/1set/gut/ystring" 10 | ) 11 | 12 | // revive:disable:exported 13 | var ( 14 | CIBuildNum string 15 | BuildDate string 16 | BuildHost string 17 | GoVersion string 18 | GitBranch string 19 | GitCommit string 20 | GitSummary string 21 | ) 22 | 23 | var ( 24 | logoArt = ` 25 | ███████╗████████╗ █████╗ ██████╗ ██╗ ███████╗████████╗ 26 | ██╔════╝╚══██╔══╝██╔══██╗██╔══██╗██║ ██╔════╝╚══██╔══╝ 27 | ███████╗ ██║ ███████║██████╔╝██║ █████╗ ██║ 28 | ╚════██║ ██║ ██╔══██║██╔══██╗██║ ██╔══╝ ██║ 29 | ███████║ ██║ ██║ ██║██║ ██║███████╗███████╗ ██║ 30 | ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ ╚═╝ 31 | ` 32 | logoArtColor = cl.RoseWaterByColumn(logoArt) 33 | ) 34 | 35 | func displayBuildInfo() { 36 | // write logo 37 | var sb strings.Builder 38 | sb.WriteString(logoArtColor) 39 | sb.WriteString(ystring.NewLine) 40 | 41 | // inline helpers 42 | arrow := "➣ " 43 | if yos.IsOnWindows() { 44 | arrow = "> " 45 | } 46 | addNonBlankField := func(name, value string) { 47 | if ystring.IsNotBlank(value) { 48 | fmt.Fprintln(&sb, arrow+name+":", value) 49 | } 50 | } 51 | 52 | addNonBlankField("Build Num ", CIBuildNum) 53 | addNonBlankField("Build Date", BuildDate) 54 | addNonBlankField("Build Host", BuildHost) 55 | addNonBlankField("Go Version", GoVersion) 56 | addNonBlankField("Git Branch", GitBranch) 57 | addNonBlankField("Git Commit", GitCommit) 58 | addNonBlankField("GitSummary", GitSummary) 59 | 60 | fmt.Println(sb.String()) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/starlet/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/1set/starlet/cmd/starlet 2 | 3 | go 1.18 4 | 5 | require ( 6 | bitbucket.org/ai69/colorlogo v0.1.1 7 | bitbucket.org/neiku/winornot v0.0.4 8 | github.com/1set/gut v0.0.0-20201117175203-a82363231997 9 | github.com/1set/starlet v0.1.0 10 | github.com/spf13/pflag v1.0.5 11 | go.starlark.net v0.0.0-20240123142251-f86470692795 12 | golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 13 | ) 14 | 15 | require ( 16 | github.com/1set/starlight v0.1.0 // indirect 17 | github.com/aymanbagabas/go-osc52 v1.0.3 // indirect 18 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 19 | github.com/gonutz/w32 v1.0.0 // indirect 20 | github.com/google/uuid v1.6.0 // indirect 21 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 22 | github.com/mattn/go-isatty v0.0.16 // indirect 23 | github.com/mattn/go-runewidth v0.0.14 // indirect 24 | github.com/muesli/termenv v0.13.0 // indirect 25 | github.com/rivo/uniseg v0.2.0 // indirect 26 | github.com/rodolfoag/gow32 v0.0.0-20160917004320-d95ff468acf8 // indirect 27 | go.uber.org/atomic v1.11.0 // indirect 28 | go.uber.org/multierr v1.9.0 // indirect 29 | go.uber.org/zap v1.24.0 // indirect 30 | golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect 31 | ) 32 | -------------------------------------------------------------------------------- /cmd/starlet/main.go: -------------------------------------------------------------------------------- 1 | // A simple example of using the Starlet REPL or running a script. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "io/fs" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | 11 | "bitbucket.org/neiku/winornot" 12 | "github.com/1set/gut/ystring" 13 | "github.com/1set/starlet" 14 | flag "github.com/spf13/pflag" 15 | "go.starlark.net/starlark" 16 | "golang.org/x/term" 17 | ) 18 | 19 | var ( 20 | allowRecursion bool 21 | allowGlobalReassign bool 22 | preloadModules []string 23 | lazyLoadModules []string 24 | includePath string 25 | codeContent string 26 | webPort uint16 27 | ) 28 | 29 | var ( 30 | defaultPreloadModules = starlet.GetAllBuiltinModuleNames() 31 | ) 32 | 33 | func init() { 34 | flag.BoolVarP(&allowRecursion, "recursion", "r", false, "allow recursion in Starlark code") 35 | flag.BoolVarP(&allowGlobalReassign, "globalreassign", "g", false, "allow reassigning global variables in Starlark code") 36 | flag.StringSliceVarP(&preloadModules, "preload", "p", defaultPreloadModules, "preload modules before executing Starlark code") 37 | flag.StringSliceVarP(&lazyLoadModules, "lazyload", "l", defaultPreloadModules, "lazy load modules when executing Starlark code") 38 | flag.StringVarP(&includePath, "include", "i", ".", "include path for Starlark code to load modules from") 39 | flag.StringVarP(&codeContent, "code", "c", "", "Starlark code to execute") 40 | flag.Uint16VarP(&webPort, "web", "w", 0, "run web server on specified port, it provides request&response structs for Starlark code to handle HTTP requests") 41 | flag.Parse() 42 | 43 | // fix for Windows terminal output 44 | winornot.EnableANSIControl() 45 | } 46 | 47 | func main() { 48 | os.Exit(processArgs()) 49 | } 50 | 51 | func processArgs() int { 52 | // get starlet machine 53 | mac := starlet.NewWithNames(nil, preloadModules, lazyLoadModules) 54 | if allowRecursion { 55 | mac.EnableRecursionSupport() 56 | } 57 | if allowGlobalReassign { 58 | mac.EnableGlobalReassign() 59 | } 60 | 61 | // for local modules 62 | var incFS fs.FS 63 | if ystring.IsNotBlank(includePath) { 64 | incFS = os.DirFS(includePath) 65 | } 66 | 67 | // check arguments 68 | nargs := flag.NArg() 69 | argCode := ystring.IsNotBlank(codeContent) 70 | switch { 71 | case webPort > 0: 72 | // run web server 73 | var setCode func(m *starlet.Machine) 74 | if argCode { 75 | // run code string from argument 76 | setCode = func(m *starlet.Machine) { 77 | m.SetScript("web.star", []byte(codeContent), incFS) 78 | } 79 | } else if nargs == 1 { 80 | // run code from file 81 | fileName := flag.Arg(0) 82 | setCode = func(m *starlet.Machine) { 83 | m.SetScript(fileName, nil, incFS) 84 | } 85 | } else { 86 | // no code to run 87 | PrintError(fmt.Errorf("no code to run as web server")) 88 | return 1 89 | } 90 | // start web server 91 | if err := runWebServer(webPort, setCode); err != nil { 92 | PrintError(err) 93 | return 1 94 | } 95 | case argCode: 96 | // run code string from argument 97 | setMachineExtras(mac, append([]string{`-c`}, flag.Args()...)) 98 | mac.SetScript("direct.star", []byte(codeContent), incFS) 99 | _, err := mac.Run() 100 | if err != nil { 101 | PrintError(err) 102 | return 1 103 | } 104 | case nargs == 0 && !argCode: 105 | // run REPL 106 | stdinIsTerminal := term.IsTerminal(int(os.Stdin.Fd())) 107 | if stdinIsTerminal { 108 | displayBuildInfo() 109 | } 110 | setMachineExtras(mac, []string{``}) 111 | mac.SetScript("repl", nil, incFS) 112 | mac.REPL() 113 | if stdinIsTerminal { 114 | fmt.Println() 115 | } 116 | case nargs >= 1: 117 | // run code from file 118 | fileName := flag.Arg(0) 119 | bs, err := ioutil.ReadFile(fileName) 120 | if err != nil { 121 | PrintError(err) 122 | return 1 123 | } 124 | setMachineExtras(mac, flag.Args()) 125 | mac.SetScript(filepath.Base(fileName), bs, incFS) 126 | if _, err := mac.Run(); err != nil { 127 | PrintError(err) 128 | return 1 129 | } 130 | default: 131 | flag.Usage() 132 | return 1 133 | } 134 | return 0 135 | } 136 | 137 | // PrintError prints the error to stderr, 138 | // or its backtrace if it is a Starlark evaluation error. 139 | func PrintError(err error) { 140 | if evalErr, ok := err.(*starlark.EvalError); ok { 141 | fmt.Fprintln(os.Stderr, evalErr.Backtrace()) 142 | } else { 143 | fmt.Fprintln(os.Stderr, err) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /cmd/starlet/mod.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/1set/starlet" 13 | "github.com/1set/starlet/dataconv" 14 | shttp "github.com/1set/starlet/lib/http" 15 | "go.starlark.net/starlark" 16 | ) 17 | 18 | func runWebServer(port uint16, setCode func(m *starlet.Machine)) error { 19 | mux := http.NewServeMux() 20 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 21 | // prepare envs 22 | resp := shttp.NewServerResponse() 23 | glb := starlet.StringAnyMap{ 24 | "request": shttp.ConvertServerRequest(r), 25 | "response": resp.Struct(), 26 | } 27 | 28 | // run code 29 | mac := starlet.NewWithNames(glb, preloadModules, lazyLoadModules) 30 | setCode(mac) 31 | _, err := mac.Run() 32 | 33 | // handle error 34 | if err != nil { 35 | log.Printf("Runtime Error: %v\n", err) 36 | w.Header().Set("Content-Type", "text/plain") 37 | w.WriteHeader(http.StatusInternalServerError) 38 | if _, err := fmt.Fprintf(w, "Runtime Error: %v", err); err != nil { 39 | log.Printf("Error writing response: %v", err) 40 | } 41 | return 42 | } 43 | 44 | // handle response 45 | if err = resp.Write(w); err != nil { 46 | w.Header().Add("Content-Type", "text/plain") 47 | w.WriteHeader(http.StatusInternalServerError) 48 | _, _ = w.Write([]byte(err.Error())) 49 | } 50 | }) 51 | 52 | log.Printf("Server is starting on port: %d\n", port) 53 | err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux) 54 | if err != nil { 55 | log.Fatalf("Server failed to start: %v", err) 56 | } 57 | return err 58 | } 59 | 60 | func runWebServerLegacy(port uint16, setCode func(m *starlet.Machine)) error { 61 | mux := http.NewServeMux() 62 | mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 63 | glb := starlet.StringAnyMap{ 64 | "reader": r, 65 | "writer": w, 66 | "fprintf": fmt.Fprintf, 67 | } 68 | 69 | mac := starlet.NewWithNames(glb, preloadModules, lazyLoadModules) 70 | setCode(mac) 71 | //mac.SetScript("web.star", code, incFS) 72 | if _, err := mac.Run(); err != nil { 73 | log.Printf("Runtime Error: %v\n", err) 74 | w.WriteHeader(http.StatusInternalServerError) 75 | if _, err := fmt.Fprintf(w, "Runtime Error: %v", err); err != nil { 76 | log.Printf("Error writing response: %v", err) 77 | } 78 | return 79 | } 80 | }) 81 | 82 | log.Printf("Server is starting on port: %d\n", port) 83 | err := http.ListenAndServe(fmt.Sprintf(":%d", port), mux) 84 | if err != nil { 85 | log.Fatalf("Server failed to start: %v", err) 86 | } 87 | return err 88 | } 89 | 90 | func setMachineExtras(m *starlet.Machine, args []string) { 91 | sysLoader := loadSysModule(args) 92 | m.AddPreloadModules(starlet.ModuleLoaderList{sysLoader}) 93 | m.AddLazyloadModules(starlet.ModuleLoaderMap{"sys": sysLoader}) 94 | } 95 | 96 | func loadSysModule(args []string) func() (starlark.StringDict, error) { 97 | // get sa 98 | sa := make([]starlark.Value, 0, len(args)) 99 | for _, arg := range args { 100 | sa = append(sa, starlark.String(arg)) 101 | } 102 | // build module 103 | sd := starlark.StringDict{ 104 | "platform": starlark.String(runtime.GOOS), 105 | "arch": starlark.String(runtime.GOARCH), 106 | "version": starlark.MakeUint(starlark.CompilerVersion), 107 | "argv": starlark.NewList(sa), 108 | "input": starlark.NewBuiltin("sys.input", rawStdInput), 109 | } 110 | return dataconv.WrapModuleData("sys", sd) 111 | } 112 | 113 | func rawStdInput(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 114 | // unpack arguments 115 | var prompt string 116 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "prompt?", &prompt); err != nil { 117 | return starlark.None, err 118 | } 119 | // display prompt 120 | if prompt != "" { 121 | fmt.Print(prompt) 122 | } 123 | // read input from stdin 124 | reader := bufio.NewReader(os.Stdin) 125 | input, err := reader.ReadString('\n') 126 | if err != nil { 127 | return nil, err 128 | } 129 | // trim newline characters 130 | input = strings.TrimRight(input, "\r\n") 131 | return starlark.String(input), nil 132 | } 133 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package starlet 2 | 3 | import ( 4 | libatom "github.com/1set/starlet/lib/atom" 5 | libb64 "github.com/1set/starlet/lib/base64" 6 | libcsv "github.com/1set/starlet/lib/csv" 7 | libfile "github.com/1set/starlet/lib/file" 8 | libgoid "github.com/1set/starlet/lib/goidiomatic" 9 | libhash "github.com/1set/starlet/lib/hashlib" 10 | libhttp "github.com/1set/starlet/lib/http" 11 | libjson "github.com/1set/starlet/lib/json" 12 | liblog "github.com/1set/starlet/lib/log" 13 | libnet "github.com/1set/starlet/lib/net" 14 | libpath "github.com/1set/starlet/lib/path" 15 | librand "github.com/1set/starlet/lib/random" 16 | libre "github.com/1set/starlet/lib/re" 17 | librt "github.com/1set/starlet/lib/runtime" 18 | libstat "github.com/1set/starlet/lib/stats" 19 | libstr "github.com/1set/starlet/lib/string" 20 | stdmath "go.starlark.net/lib/math" 21 | stdtime "go.starlark.net/lib/time" 22 | "go.starlark.net/resolve" 23 | "go.starlark.net/starlark" 24 | stdstruct "go.starlark.net/starlarkstruct" 25 | ) 26 | 27 | var allBuiltinModules = ModuleLoaderMap{ 28 | libgoid.ModuleName: func() (starlark.StringDict, error) { 29 | return libgoid.LoadModule() 30 | }, 31 | "math": func() (starlark.StringDict, error) { 32 | return starlark.StringDict{ 33 | "math": stdmath.Module, 34 | }, nil 35 | }, 36 | "struct": func() (starlark.StringDict, error) { 37 | return starlark.StringDict{ 38 | "struct": starlark.NewBuiltin("struct", stdstruct.Make), 39 | }, nil 40 | }, 41 | "time": func() (starlark.StringDict, error) { 42 | return starlark.StringDict{ 43 | "time": stdtime.Module, 44 | }, nil 45 | }, 46 | // add third-party modules 47 | libatom.ModuleName: libatom.LoadModule, 48 | libb64.ModuleName: libb64.LoadModule, 49 | libcsv.ModuleName: libcsv.LoadModule, 50 | libfile.ModuleName: libfile.LoadModule, 51 | libhash.ModuleName: libhash.LoadModule, 52 | libhttp.ModuleName: libhttp.LoadModule, 53 | libnet.ModuleName: libnet.LoadModule, 54 | libjson.ModuleName: libjson.LoadModule, 55 | liblog.ModuleName: liblog.LoadModule, 56 | libpath.ModuleName: libpath.LoadModule, 57 | librand.ModuleName: librand.LoadModule, 58 | libre.ModuleName: libre.LoadModule, 59 | librt.ModuleName: librt.LoadModule, 60 | libstr.ModuleName: libstr.LoadModule, 61 | libstat.ModuleName: libstat.LoadModule, 62 | } 63 | 64 | // GetAllBuiltinModuleNames returns a list of all builtin module names. 65 | func GetAllBuiltinModuleNames() []string { 66 | return allBuiltinModules.Keys() 67 | } 68 | 69 | // GetAllBuiltinModules returns a list of all builtin modules. 70 | func GetAllBuiltinModules() ModuleLoaderList { 71 | return allBuiltinModules.Values() 72 | } 73 | 74 | // GetBuiltinModuleMap returns a map of all builtin modules. 75 | func GetBuiltinModuleMap() ModuleLoaderMap { 76 | return allBuiltinModules.Clone() 77 | } 78 | 79 | // GetBuiltinModule returns the builtin module with the given name. 80 | func GetBuiltinModule(name string) ModuleLoader { 81 | return allBuiltinModules[name] 82 | } 83 | 84 | // EnableRecursionSupport enables recursion support in Starlark environments for loading modules. 85 | func EnableRecursionSupport() { 86 | resolve.AllowRecursion = true 87 | } 88 | 89 | // DisableRecursionSupport disables recursion support in Starlark environments for loading modules. 90 | func DisableRecursionSupport() { 91 | resolve.AllowRecursion = false 92 | } 93 | 94 | // EnableGlobalReassign enables global reassignment in Starlark environments for loading modules. 95 | func EnableGlobalReassign() { 96 | resolve.AllowGlobalReassign = true 97 | } 98 | 99 | // DisableGlobalReassign disables global reassignment in Starlark environments for loading modules. 100 | func DisableGlobalReassign() { 101 | resolve.AllowGlobalReassign = false 102 | } 103 | -------------------------------------------------------------------------------- /dataconv/interface.go: -------------------------------------------------------------------------------- 1 | // Package dataconv provides helper functions to convert between Starlark and Go types. 2 | // 3 | // It works like package starlight, but only supports common Starlark and Go types, and won't wrap any custom types or functions. 4 | // 5 | // For data type conversion, it provides functions to convert between Starlark and Go types: 6 | // 7 | // +---------+ Marshal +------------+ MarshalStarlarkJSON +----------+ 8 | // | | ----------> | | ----------------------> | | 9 | // | Go | | Starlark | | JSON | 10 | // | Value | <---------- | Value | <---------------------- | String | 11 | // | | Unmarshal | | UnmarshalStarlarkJSON | | 12 | // +---------+ +------------+ +----------+ 13 | // 14 | package dataconv 15 | 16 | import "go.starlark.net/starlark" 17 | 18 | // StarlarkFunc is a function that can be called from Starlark. 19 | type StarlarkFunc func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) 20 | 21 | // Unmarshaler is the interface use to unmarshal Starlark values to custom types, i.e. Starlark to Go. 22 | type Unmarshaler interface { 23 | // UnmarshalStarlark unmarshal a Starlark object to custom type. 24 | UnmarshalStarlark(starlark.Value) error 25 | } 26 | 27 | // Marshaler is the interface use to marshal Starlark from custom types, i.e. Go to Starlark. 28 | type Marshaler interface { 29 | // MarshalStarlark marshal a custom type to Starlark object. 30 | MarshalStarlark() (starlark.Value, error) 31 | } 32 | -------------------------------------------------------------------------------- /dataconv/types/either.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.starlark.net/starlark" 7 | ) 8 | 9 | // EitherOrNone is an Unpacker that converts a Starlark None, A, or B to Go's starlark.Value. 10 | type EitherOrNone[A starlark.Value, B starlark.Value] struct { 11 | value starlark.Value 12 | isNone bool 13 | isTypeA bool 14 | isTypeB bool 15 | } 16 | 17 | // NewEitherOrNone creates and returns a new EitherOrNone. 18 | func NewEitherOrNone[A starlark.Value, B starlark.Value]() *EitherOrNone[A, B] { 19 | return &EitherOrNone[A, B]{isNone: true} 20 | } 21 | 22 | // Unpack implements the starlark.Unpacker interface. 23 | func (e *EitherOrNone[A, B]) Unpack(v starlark.Value) error { 24 | if e == nil { 25 | return errNilReceiver 26 | } 27 | if _, ok := v.(starlark.NoneType); ok { 28 | e.value = nil 29 | e.isNone, e.isTypeA, e.isTypeB = true, false, false 30 | } else if a, ok := v.(A); ok { 31 | e.value = a 32 | e.isNone, e.isTypeA, e.isTypeB = false, true, false 33 | } else if b, ok := v.(B); ok { 34 | e.value = b 35 | e.isNone, e.isTypeA, e.isTypeB = false, false, true 36 | } else { 37 | var zeroA A 38 | var zeroB B 39 | return fmt.Errorf("expected %T or %T or None, got %T (%s)", zeroA, zeroB, v, gotStarType(v)) 40 | } 41 | return nil 42 | } 43 | 44 | // IsNone returns true if the value is None. 45 | func (e *EitherOrNone[A, B]) IsNone() bool { 46 | return e != nil && e.isNone 47 | } 48 | 49 | // IsTypeA returns true if the value is of type A. 50 | func (e *EitherOrNone[A, B]) IsTypeA() bool { 51 | return e != nil && e.isTypeA 52 | } 53 | 54 | // IsTypeB returns true if the value is of type B. 55 | func (e *EitherOrNone[A, B]) IsTypeB() bool { 56 | return e != nil && e.isTypeB 57 | } 58 | 59 | // Value returns the underlying value. You can use IsTypeA and IsTypeB to check which type it is. 60 | func (e *EitherOrNone[A, B]) Value() starlark.Value { 61 | if e == nil { 62 | var zero starlark.Value 63 | return zero 64 | } 65 | return e.value 66 | } 67 | 68 | // ValueA returns the value of type A, if available, and a boolean indicating its presence. 69 | func (e *EitherOrNone[A, B]) ValueA() (A, bool) { 70 | if e != nil && e.isTypeA { 71 | return e.value.(A), true 72 | } 73 | var zero A 74 | return zero, false 75 | } 76 | 77 | // ValueB returns the value of type B, if available, and a boolean indicating its presence. 78 | func (e *EitherOrNone[A, B]) ValueB() (B, bool) { 79 | if e != nil && e.isTypeB { 80 | return e.value.(B), true 81 | } 82 | var zero B 83 | return zero, false 84 | } 85 | 86 | // Type returns the type of the underlying value. 87 | func (e *EitherOrNone[A, B]) Type() string { 88 | if e == nil { 89 | return "NilReceiver" 90 | } 91 | if e.isNone { 92 | return starlark.None.Type() 93 | } 94 | if e.isTypeA { 95 | var a A 96 | return a.Type() 97 | } 98 | if e.isTypeB { 99 | var b B 100 | return b.Type() 101 | } 102 | return "Unknown" 103 | } 104 | 105 | // Unpacker interface implementation check 106 | var ( 107 | _ starlark.Unpacker = (*EitherOrNone[*starlark.List, *starlark.Dict])(nil) 108 | _ starlark.Unpacker = (*EitherOrNone[starlark.String, starlark.Int])(nil) 109 | ) 110 | -------------------------------------------------------------------------------- /dataconv/types/many.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.starlark.net/starlark" 7 | ) 8 | 9 | // OneOrMany is a struct that can hold either a single value or multiple values of a specific type, and can be unpacked from a Starlark value. 10 | type OneOrMany[T starlark.Value] struct { 11 | values []T 12 | defaultValue T 13 | hasDefault bool 14 | } 15 | 16 | // NewOneOrMany creates and returns a new OneOrMany with the given default value. 17 | func NewOneOrMany[T starlark.Value](defaultValue T) *OneOrMany[T] { 18 | return &OneOrMany[T]{values: nil, defaultValue: defaultValue, hasDefault: true} 19 | } 20 | 21 | // NewOneOrManyNoDefault creates and returns a new OneOrMany without a default value. 22 | func NewOneOrManyNoDefault[T starlark.Value]() *OneOrMany[T] { 23 | return &OneOrMany[T]{values: nil, hasDefault: false} 24 | } 25 | 26 | // Unpack implements the starlark.Unpacker interface, allowing the struct to unpack from a starlark.Value. 27 | func (o *OneOrMany[T]) Unpack(v starlark.Value) error { 28 | if o == nil { 29 | return errNilReceiver 30 | } 31 | if _, ok := v.(starlark.NoneType); ok { 32 | // None 33 | o.values = nil 34 | } else if t, ok := v.(T); ok { 35 | // Single value 36 | o.values = []T{t} 37 | } else if l, ok := v.(starlark.Iterable); ok { 38 | // List or Tuple or Set of values 39 | sl := make([]T, 0, 1) 40 | iter := l.Iterate() 41 | defer iter.Done() 42 | // Iterate over the iterable 43 | var x starlark.Value 44 | for iter.Next(&x) { 45 | if t, ok := x.(T); ok { 46 | sl = append(sl, t) 47 | } else { 48 | return fmt.Errorf("expected %T, got %s", o.defaultValue, gotStarType(x)) 49 | } 50 | } 51 | o.values = sl 52 | } else { 53 | return fmt.Errorf("expected %T or Iterable or None, got %s", o.defaultValue, gotStarType(v)) 54 | } 55 | return nil 56 | } 57 | 58 | // IsNull checks if the struct is nil or has no underlying slice. It returns true if the struct is nil or has no underlying slice, no matter if a default value is set. 59 | func (o *OneOrMany[T]) IsNull() bool { 60 | return o == nil || o.values == nil 61 | } 62 | 63 | // Len returns the length of the underlying slice or default value. 64 | func (o *OneOrMany[T]) Len() int { 65 | if o == nil { 66 | return 0 67 | } 68 | if o.values == nil { 69 | if o.hasDefault { 70 | return 1 71 | } 72 | return 0 73 | } 74 | return len(o.values) 75 | } 76 | 77 | // Slice returns the underlying slice, or a slice containing the default value if the slice is nil and a default is set. 78 | // It returns an empty slice if the underlying slice is nil and has no default value. 79 | func (o *OneOrMany[T]) Slice() []T { 80 | if o == nil { 81 | return []T{} 82 | } 83 | if o.values == nil { 84 | if o.hasDefault { 85 | return []T{o.defaultValue} 86 | } 87 | return []T{} 88 | } 89 | return o.values 90 | } 91 | 92 | // First returns the first element in the slice, or the default value if the slice is nil or empty and a default is set. 93 | // It returns the zero value of the type if the underlying slice is nil and has no default value. 94 | func (o *OneOrMany[T]) First() T { 95 | if o == nil || o.values == nil || len(o.values) == 0 { 96 | if o != nil && o.hasDefault { 97 | return o.defaultValue 98 | } 99 | var zero T 100 | return zero 101 | } 102 | return o.values[0] 103 | } 104 | 105 | // Unpacker is an interface for types that can be unpacked from Starlark values. 106 | var ( 107 | _ starlark.Unpacker = (*OneOrMany[starlark.Int])(nil) 108 | _ starlark.Unpacker = (*OneOrMany[starlark.String])(nil) 109 | _ starlark.Unpacker = (*OneOrMany[*starlark.Dict])(nil) 110 | ) 111 | -------------------------------------------------------------------------------- /dataconv/types/nullable.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.starlark.net/starlark" 7 | ) 8 | 9 | // Nullable is an Unpacker that converts a Starlark None or T to Go's starlark.Value. 10 | type Nullable[T starlark.Value] struct { 11 | value *T 12 | defaultValue T 13 | } 14 | 15 | // NewNullable creates and returns a new Nullable with the given default value. 16 | func NewNullable[T starlark.Value](defaultValue T) *Nullable[T] { 17 | return &Nullable[T]{value: nil, defaultValue: defaultValue} 18 | } 19 | 20 | // Unpack implements Unpacker. 21 | func (p *Nullable[T]) Unpack(v starlark.Value) error { 22 | if p == nil { 23 | return errNilReceiver 24 | } 25 | if _, ok := v.(starlark.NoneType); ok { 26 | p.value = nil 27 | } else if t, ok := v.(T); ok { 28 | p.value = &t 29 | } else { 30 | return fmt.Errorf("expected %T or None, got %s", p.defaultValue, gotStarType(v)) 31 | } 32 | return nil 33 | } 34 | 35 | // IsNull returns true if the underlying value is nil. 36 | func (p *Nullable[T]) IsNull() bool { 37 | return p == nil || p.value == nil 38 | } 39 | 40 | // Value returns the underlying value or default value if the underlying value is nil. 41 | func (p *Nullable[T]) Value() T { 42 | if p.IsNull() { 43 | return p.defaultValue 44 | } 45 | return *p.value 46 | } 47 | 48 | type ( 49 | // NullableInt is an Unpacker that converts a Starlark None or Int. 50 | NullableInt = Nullable[starlark.Int] 51 | 52 | // NullableFloat is an Unpacker that converts a Starlark None or Float. 53 | NullableFloat = Nullable[starlark.Float] 54 | 55 | // NullableBool is an Unpacker that converts a Starlark None or Bool. 56 | NullableBool = Nullable[starlark.Bool] 57 | 58 | // NullableString is an Unpacker that converts a Starlark None or String. 59 | NullableString = Nullable[starlark.String] 60 | 61 | // NullableBytes is an Unpacker that converts a Starlark None or Bytes. 62 | NullableBytes = Nullable[starlark.Bytes] 63 | 64 | // NullableList is an Unpacker that converts a Starlark None or List. 65 | NullableList = Nullable[*starlark.List] 66 | 67 | // NullableTuple is an Unpacker that converts a Starlark None or Tuple. 68 | NullableTuple = Nullable[starlark.Tuple] 69 | 70 | // NullableSet is an Unpacker that converts a Starlark None or Set. 71 | NullableSet = Nullable[*starlark.Set] 72 | 73 | // NullableDict is an Unpacker that converts a Starlark None or Dict. 74 | NullableDict = Nullable[*starlark.Dict] 75 | 76 | // NullableIterable is an Unpacker that converts a Starlark None or Iterable. 77 | NullableIterable = Nullable[starlark.Iterable] 78 | 79 | // NullableCallable is an Unpacker that converts a Starlark None or Callable. 80 | NullableCallable = Nullable[starlark.Callable] 81 | ) 82 | 83 | // Unpacker is an interface for types that can be unpacked from Starlark values. 84 | var ( 85 | _ starlark.Unpacker = (*NullableInt)(nil) 86 | _ starlark.Unpacker = (*NullableFloat)(nil) 87 | _ starlark.Unpacker = (*NullableBool)(nil) 88 | _ starlark.Unpacker = (*NullableString)(nil) 89 | _ starlark.Unpacker = (*NullableBytes)(nil) 90 | _ starlark.Unpacker = (*NullableList)(nil) 91 | _ starlark.Unpacker = (*NullableTuple)(nil) 92 | _ starlark.Unpacker = (*NullableSet)(nil) 93 | _ starlark.Unpacker = (*NullableDict)(nil) 94 | _ starlark.Unpacker = (*NullableCallable)(nil) 95 | _ starlark.Unpacker = (*NullableIterable)(nil) 96 | ) 97 | 98 | var ( 99 | // NewNullableInt creates and returns a new NullableInt with the given default value. 100 | NewNullableInt = func(dv starlark.Int) *NullableInt { return NewNullable[starlark.Int](dv) } 101 | 102 | // NewNullableFloat creates and returns a new NullableFloat with the given default value. 103 | NewNullableFloat = func(dv starlark.Float) *NullableFloat { return NewNullable[starlark.Float](dv) } 104 | 105 | // NewNullableBool creates and returns a new NullableBool with the given default value. 106 | NewNullableBool = func(dv starlark.Bool) *NullableBool { return NewNullable[starlark.Bool](dv) } 107 | 108 | // NewNullableString creates and returns a new NullableString with the given default value. 109 | NewNullableString = func(dv starlark.String) *NullableString { return NewNullable[starlark.String](dv) } 110 | 111 | // NewNullableBytes creates and returns a new NullableBytes with the given default value. 112 | NewNullableBytes = func(dv starlark.Bytes) *NullableBytes { return NewNullable[starlark.Bytes](dv) } 113 | 114 | // NewNullableList creates and returns a new NullableList with the given default value. 115 | NewNullableList = func(dv *starlark.List) *NullableList { return NewNullable[*starlark.List](dv) } 116 | 117 | // NewNullableTuple creates and returns a new NullableTuple with the given default value. 118 | NewNullableTuple = func(dv starlark.Tuple) *NullableTuple { return NewNullable[starlark.Tuple](dv) } 119 | 120 | // NewNullableSet creates and returns a new NullableSet with the given default value. 121 | NewNullableSet = func(dv *starlark.Set) *NullableSet { return NewNullable[*starlark.Set](dv) } 122 | 123 | // NewNullableDict creates and returns a new NullableDict with the given default value. 124 | NewNullableDict = func(dv *starlark.Dict) *NullableDict { return NewNullable[*starlark.Dict](dv) } 125 | 126 | // NewNullableIterable creates and returns a new NullableIterable with the given default value. 127 | NewNullableIterable = func(dv starlark.Iterable) *NullableIterable { return NewNullable[starlark.Iterable](dv) } 128 | 129 | // NewNullableCallable creates and returns a new NullableCallable with the given default value. 130 | NewNullableCallable = func(dv starlark.Callable) *NullableCallable { return NewNullable[starlark.Callable](dv) } 131 | ) 132 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package starlet 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.starlark.net/starlark" 7 | ) 8 | 9 | // ExecError is a custom error type for Starlet execution errors. 10 | type ExecError struct { 11 | pkg string // dependency source package or component name 12 | act string // error happens when doing this action 13 | cause error // the cause of the error 14 | hint string // additional hint for the error 15 | } 16 | 17 | // Unwrap returns the cause of the error. 18 | func (e ExecError) Unwrap() error { 19 | return e.cause 20 | } 21 | 22 | // Error returns the error message. 23 | func (e ExecError) Error() string { 24 | s := fmt.Sprintf("%s: %s: %v", e.pkg, e.act, e.cause) 25 | if e.hint != "" { 26 | s += "\n" + e.hint 27 | } 28 | return s 29 | } 30 | 31 | // helper functions 32 | 33 | // errorStarlarkPanic creates an ExecError from a recovered panic value. 34 | func errorStarlarkPanic(action string, v interface{}) ExecError { 35 | return ExecError{ 36 | pkg: `starlark`, 37 | act: action, 38 | cause: fmt.Errorf("panic: %v", v), 39 | } 40 | } 41 | 42 | // errorStarlarkError creates an ExecError from a Starlark error and an related action. 43 | func errorStarlarkError(action string, err error) ExecError { 44 | // don't wrap if the error is already an ExecError 45 | if e, ok := err.(ExecError); ok { 46 | return e 47 | } 48 | // parse error from Starlark 49 | var hint string 50 | if se, ok := err.(*starlark.EvalError); ok { 51 | hint = se.Backtrace() 52 | } 53 | return ExecError{ 54 | pkg: `starlark`, 55 | act: action, 56 | cause: err, 57 | hint: hint, 58 | } 59 | } 60 | 61 | // errorStarletError creates an ExecError for starlet. 62 | func errorStarletError(action string, err error) ExecError { 63 | // don't wrap if the error is already an ExecError 64 | if e, ok := err.(ExecError); ok { 65 | return e 66 | } 67 | return ExecError{ 68 | pkg: `starlet`, 69 | act: action, 70 | cause: err, 71 | } 72 | } 73 | 74 | // errorStarletErrorf creates an ExecError for starlet with a formatted message. 75 | func errorStarletErrorf(action string, format string, args ...interface{}) ExecError { 76 | return ExecError{ 77 | pkg: `starlet`, 78 | act: action, 79 | cause: fmt.Errorf(format, args...), 80 | } 81 | } 82 | 83 | // errorStarlightConvert creates an ExecError for starlight data conversion. 84 | func errorStarlightConvert(target string, err error) ExecError { 85 | // don't wrap if the error is already an ExecError 86 | if e, ok := err.(ExecError); ok { 87 | return e 88 | } 89 | return ExecError{ 90 | pkg: `starlight`, 91 | act: fmt.Sprintf("convert %s", target), 92 | cause: err, 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /exec.go: -------------------------------------------------------------------------------- 1 | package starlet 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "sync" 8 | 9 | itn "github.com/1set/starlet/internal" 10 | "go.starlark.net/starlark" 11 | ) 12 | 13 | // execStarlarkFile executes a Starlark file with the given filename and source, and returns the global environment and any error encountered. 14 | // If the cache is enabled, it will try to load the compiled program from the cache first, and save the compiled program to the cache after compilation. 15 | func (m *Machine) execStarlarkFile(filename string, src interface{}, allowCache bool) (starlark.StringDict, error) { 16 | // restore the arguments for starlark.ExecFileOptions 17 | opts := m.getFileOptions() 18 | thread := m.thread 19 | predeclared := m.predeclared 20 | hasCache := m.progCache != nil 21 | 22 | // if cache is not enabled or not allowed, just execute the original source 23 | if !hasCache || !allowCache { 24 | return starlark.ExecFileOptions(opts, thread, filename, src, predeclared) 25 | } 26 | 27 | // for compiled program and cache key 28 | var ( 29 | prog *starlark.Program 30 | err error 31 | //key = fmt.Sprintf("%d:%s", starlark.CompilerVersion, filename) 32 | key = getCacheKey(filename, src) 33 | ) 34 | 35 | // try to load compiled program from cache first 36 | if hasCache { 37 | // if cache is enabled, try to load compiled bytes from cache first 38 | if cb, ok := m.progCache.Get(key); ok { 39 | // load program from compiled bytes 40 | if prog, err = starlark.CompiledProgram(bytes.NewReader(cb)); err != nil { 41 | // if failed, remove the result and continue 42 | prog = nil 43 | } 44 | } 45 | } 46 | 47 | // if program is not loaded from cache, compile and cache it 48 | if prog == nil { 49 | // parse, resolve, and compile a Starlark source file. 50 | if _, prog, err = starlark.SourceProgramOptions(opts, filename, src, predeclared.Has); err != nil { 51 | return nil, err 52 | } 53 | // dump the compiled program to bytes 54 | buf := new(bytes.Buffer) 55 | if err = prog.Write(buf); err != nil { 56 | return nil, err 57 | } 58 | // save the compiled bytes to cache 59 | _ = m.progCache.Set(key, buf.Bytes()) 60 | } 61 | 62 | // execute the compiled program 63 | g, err := prog.Init(thread, predeclared) 64 | g.Freeze() 65 | return g, err 66 | } 67 | 68 | func getCacheKey(filename string, src interface{}) string { 69 | var k string 70 | switch s := src.(type) { 71 | case string: 72 | k = itn.GetStringMD5(s) 73 | case []byte: 74 | k = itn.GetBytesMD5(s) 75 | default: 76 | k = filename 77 | } 78 | return fmt.Sprintf("%d:%s", starlark.CompilerVersion, k) 79 | } 80 | 81 | // ByteCache is an interface for caching byte data, used for caching compiled Starlark programs. 82 | type ByteCache interface { 83 | Get(key string) ([]byte, bool) 84 | Set(key string, value []byte) error 85 | } 86 | 87 | // MemoryCache is a simple in-memory map-based ByteCache, serves as a default cache for Starlark programs. 88 | type MemoryCache struct { 89 | _ itn.DoNotCompare 90 | sync.RWMutex 91 | data map[string][]byte 92 | } 93 | 94 | // NewMemoryCache creates a new MemoryCache instance. 95 | func NewMemoryCache() *MemoryCache { 96 | return &MemoryCache{ 97 | data: make(map[string][]byte), 98 | } 99 | } 100 | 101 | // Get returns the value for the given key, and whether the key exists. 102 | func (c *MemoryCache) Get(key string) ([]byte, bool) { 103 | c.RLock() 104 | defer c.RUnlock() 105 | 106 | if c == nil || c.data == nil { 107 | return nil, false 108 | } 109 | v, ok := c.data[key] 110 | return v, ok 111 | } 112 | 113 | // Set sets the value for the given key. 114 | func (c *MemoryCache) Set(key string, value []byte) error { 115 | c.Lock() 116 | defer c.Unlock() 117 | 118 | if c == nil || c.data == nil { 119 | return errors.New("no data map found in the cache") 120 | } 121 | c.data[key] = value 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /exec_test.go: -------------------------------------------------------------------------------- 1 | package starlet_test 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/1set/starlet" 8 | itn "github.com/1set/starlet/internal" 9 | ) 10 | 11 | func TestNewMemoryCache(t *testing.T) { 12 | mc := starlet.NewMemoryCache() 13 | if mc == nil { 14 | t.Errorf("NewMemoryCache() = nil; want not nil") 15 | } 16 | } 17 | 18 | func TestMemoryCache_Get(t *testing.T) { 19 | mc := starlet.NewMemoryCache() 20 | mc.Set("test", []byte("value")) 21 | 22 | tests := []struct { 23 | name string 24 | mc *starlet.MemoryCache 25 | key string 26 | wantData []byte 27 | wantHit bool 28 | }{ 29 | { 30 | name: "Key exists", 31 | mc: mc, 32 | key: "test", 33 | wantData: []byte("value"), 34 | wantHit: true, 35 | }, 36 | { 37 | name: "Key does not exist", 38 | mc: mc, 39 | key: "nonsense", 40 | wantData: nil, 41 | wantHit: false, 42 | }, 43 | } 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | data, ok := tt.mc.Get(tt.key) 47 | if !reflect.DeepEqual(data, tt.wantData) { 48 | t.Errorf("MemoryCache.Get() got = %v, want data %v", data, tt.wantData) 49 | } 50 | if ok != tt.wantHit { 51 | t.Errorf("MemoryCache.Get() got = %v, want hit %v", ok, tt.wantHit) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func TestMemoryCache_Set(t *testing.T) { 58 | mc := starlet.NewMemoryCache() 59 | 60 | tests := []struct { 61 | name string 62 | mc *starlet.MemoryCache 63 | key string 64 | value []byte 65 | wantErr bool 66 | }{ 67 | { 68 | name: "Valid Key-Value", 69 | mc: mc, 70 | key: "test", 71 | value: []byte("value"), 72 | wantErr: false, 73 | }, 74 | { 75 | name: "Invalid MemoryCache", 76 | mc: &starlet.MemoryCache{}, 77 | key: "test", 78 | value: []byte("value"), 79 | wantErr: true, 80 | }, 81 | } 82 | for _, tt := range tests { 83 | t.Run(tt.name, func(t *testing.T) { 84 | err := tt.mc.Set(tt.key, tt.value) 85 | if (err != nil) != tt.wantErr { 86 | t.Errorf("MemoryCache.Set() error = %v, wantErr %v", err, tt.wantErr) 87 | return 88 | } 89 | // If no error, assert that the value was set correctly 90 | if !tt.wantErr { 91 | got, _ := tt.mc.Get(tt.key) 92 | if !reflect.DeepEqual(got, tt.value) { 93 | t.Errorf("MemoryCache.Set() = %v, want %v", got, tt.value) 94 | } 95 | } 96 | }) 97 | } 98 | } 99 | 100 | var ( 101 | testScriptName = "test" 102 | testScriptBytes = []byte(itn.HereDoc(` 103 | # This is a test script 104 | a = 10 105 | b = 20 106 | def add(x, y): 107 | return x+y 108 | c = add(a,b) 109 | `)) 110 | ) 111 | 112 | func BenchmarkRunNoCache(b *testing.B) { 113 | m := starlet.NewDefault() 114 | m.SetScript(testScriptName, testScriptBytes, nil) 115 | if _, err := m.Run(); err != nil { 116 | b.Errorf("Run() error = %v", err) 117 | return 118 | } 119 | 120 | b.ResetTimer() 121 | for i := 0; i < b.N; i++ { 122 | _, _ = m.Run() 123 | } 124 | } 125 | 126 | func BenchmarkRunMemoryCache(b *testing.B) { 127 | m := starlet.NewDefault() 128 | m.SetScriptCacheEnabled(true) 129 | m.SetScript(testScriptName, testScriptBytes, nil) 130 | if _, err := m.Run(); err != nil { 131 | b.Errorf("Run() error = %v", err) 132 | return 133 | } 134 | 135 | b.ResetTimer() 136 | for i := 0; i < b.N; i++ { 137 | _, _ = m.Run() 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/1set/starlet 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/1set/starlight v0.1.2 7 | github.com/google/uuid v1.6.0 8 | github.com/h2so5/here v0.0.0-20200815043652-5e14eb691fae 9 | github.com/montanaflynn/stats v0.7.1 10 | github.com/spyzhov/ajson v0.9.6 11 | go.starlark.net v0.0.0-20240123142251-f86470692795 12 | go.uber.org/atomic v1.11.0 13 | go.uber.org/zap v1.24.0 14 | ) 15 | 16 | require ( 17 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 18 | go.uber.org/multierr v1.9.0 // indirect 19 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /internal/doc.go: -------------------------------------------------------------------------------- 1 | // Package internal contains types and utilities that are not part of the public API, and may change without notice. 2 | // It should be only imported by the custom Starlark modules under starlet/lib folders, and not by the Starlet main package to avoid cyclic import. 3 | package internal 4 | 5 | import "github.com/h2so5/here" 6 | 7 | // StarletVersion should be the current version of Starlet. 8 | const StarletVersion = "v0.1.0" 9 | 10 | var ( 11 | // HereDoc returns un-indented string as here-document. 12 | HereDoc = here.Doc 13 | 14 | // HereDocf returns unindented and formatted string as here-document. Formatting is done as for fmt.Printf(). 15 | HereDocf = here.Docf 16 | ) 17 | 18 | // DoNotCompare prevents struct comparisons when embedded. It disallows the use of == and != operators. 19 | type DoNotCompare [0]func() 20 | -------------------------------------------------------------------------------- /internal/hash.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | ) 7 | 8 | // GetBytesMD5 returns the MD5 hash of the given data as a hex string. 9 | func GetBytesMD5(data []byte) string { 10 | hash := md5.New() 11 | hash.Write(data) 12 | return hex.EncodeToString(hash.Sum(nil)) 13 | } 14 | 15 | // GetStringMD5 returns the MD5 hash of the given string as a hex string. 16 | func GetStringMD5(data string) string { 17 | return GetBytesMD5([]byte(data)) 18 | } 19 | -------------------------------------------------------------------------------- /internal/hash_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "testing" 4 | 5 | func TestGetBytesMD5(t *testing.T) { 6 | tests := []struct { 7 | name string 8 | data []byte 9 | want string 10 | }{ 11 | { 12 | name: "Test 0", 13 | data: []byte(""), 14 | want: "d41d8cd98f00b204e9800998ecf8427e", 15 | }, 16 | { 17 | name: "Test 1", 18 | data: []byte("test"), 19 | want: "098f6bcd4621d373cade4e832627b4f6", 20 | }, 21 | { 22 | name: "Test 2", 23 | data: []byte("hello world"), 24 | want: "5eb63bbbe01eeed093cb22bb8f5acdc3", 25 | }, 26 | { 27 | name: "Test 3", 28 | data: []byte("The quick brown fox jumps over the lazy dog"), 29 | want: "9e107d9d372bb6826bd81d3542a419d6", 30 | }, 31 | } 32 | 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | if got := GetBytesMD5(tt.data); got != tt.want { 36 | t.Errorf("GetBytesMD5() = %v, want %v", got, tt.want) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestGetStringMD5(t *testing.T) { 43 | tests := []struct { 44 | name string 45 | data string 46 | want string 47 | }{ 48 | { 49 | name: "Test 0", 50 | data: "", 51 | want: "d41d8cd98f00b204e9800998ecf8427e", 52 | }, 53 | { 54 | name: "Test 1", 55 | data: "test", 56 | want: "098f6bcd4621d373cade4e832627b4f6", 57 | }, 58 | { 59 | name: "Test 2", 60 | data: "hello world", 61 | want: "5eb63bbbe01eeed093cb22bb8f5acdc3", 62 | }, 63 | { 64 | name: "Test 3", 65 | data: "The quick brown fox jumps over the lazy dog", 66 | want: "9e107d9d372bb6826bd81d3542a419d6", 67 | }, 68 | } 69 | 70 | for _, tt := range tests { 71 | t.Run(tt.name, func(t *testing.T) { 72 | if got := GetStringMD5(tt.data); got != tt.want { 73 | t.Errorf("GetStringMD5() = %v, want %v", got, tt.want) 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/replacecr/replace_cr.go: -------------------------------------------------------------------------------- 1 | // Package replacecr defines a wrapper for replacing solo carriage return characters (\r) with carriage-return + line feed (\r\n). 2 | // Copied from https://github.com/qri-io/starlib/tree/master/util/replacecr 3 | package replacecr 4 | 5 | import ( 6 | "bufio" 7 | "io" 8 | ) 9 | 10 | // Reader wraps an io.Reader. on every call of Read. it looks for 11 | // for instances of lonely \r replacing them with \r\n before returning to the end consumer 12 | // lots of files in the wild will come without "proper" line breaks, which irritates go's 13 | // standard csv package. This'll fix by wrapping the reader passed to csv.NewReader: 14 | // rdr, err := csv.NewReader(replacecr.Reader(r)) 15 | // because Reader adds '\n' characters, the number of bytes reported from the underlying 16 | // reader can/will differ from what the underlyng reader would return 17 | // if read from directly. This can cause issues with checksums and byte counts. 18 | // Use with caution. 19 | func Reader(data io.Reader) io.Reader { 20 | return crlfReplaceReader{ 21 | rdr: bufio.NewReader(data), 22 | } 23 | } 24 | 25 | // crlfReplaceReader wraps a reader 26 | type crlfReplaceReader struct { 27 | rdr *bufio.Reader 28 | } 29 | 30 | // Read implements io.Reader for crlfReplaceReader 31 | func (c crlfReplaceReader) Read(p []byte) (n int, err error) { 32 | lenP := len(p) 33 | if lenP == 0 { 34 | return 35 | } 36 | 37 | for { 38 | if n == lenP { 39 | return 40 | } 41 | 42 | p[n], err = c.rdr.ReadByte() 43 | if err != nil { 44 | return 45 | } 46 | 47 | // any time we encounter \r & still have space, check to see if \n follows 48 | // ff next char is not \n, add it in manually 49 | if p[n] == '\r' && n < lenP-1 { 50 | if pk, err := c.rdr.Peek(1); (err == nil && pk[0] != '\n') || (err != nil && err.Error() == "EOF") { 51 | n++ 52 | p[n] = '\n' 53 | } 54 | } 55 | 56 | n++ 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /internal/replacecr/replace_cr_test.go: -------------------------------------------------------------------------------- 1 | package replacecr 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestReader(t *testing.T) { 9 | input := []byte("foo\r\rbar\r\nbaz\r\r") 10 | expect := []byte("foo\r\n\r\nbar\r\nbaz\r\n\r\n") 11 | 12 | got := make([]byte, 19) 13 | n, err := Reader(bytes.NewReader(input)).Read(got) 14 | if err != nil && err.Error() != "EOF" { 15 | t.Errorf("unexpected error: %s", err.Error()) 16 | } 17 | if n != 19 { 18 | t.Errorf("length error. expected: %d, got: %d", 19, n) 19 | } 20 | if !bytes.Equal(expect, got) { 21 | t.Errorf("byte mismatch. expected:\n%v\ngot:\n%v", expect, got) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/testloader.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "runtime" 7 | "strings" 8 | "sync" 9 | "testing" 10 | 11 | "go.starlark.net/starlark" 12 | "go.starlark.net/starlarkstruct" 13 | "go.starlark.net/starlarktest" 14 | "go.starlark.net/syntax" 15 | ) 16 | 17 | // ModuleLoadFunc is a function that loads a Starlark module and returns the module's string dict. 18 | type ModuleLoadFunc func() (starlark.StringDict, error) 19 | 20 | // ThreadLoadFunc is a function that loads a Starlark module by name, usually used by the Starlark thread. 21 | type ThreadLoadFunc func(thread *starlark.Thread, module string) (starlark.StringDict, error) 22 | 23 | var initTestOnce sync.Once 24 | 25 | // NewAssertLoader creates a Starlark thread loader that loads a module by name or asserts.star for testing. 26 | func NewAssertLoader(moduleName string, loader ModuleLoadFunc) ThreadLoadFunc { 27 | initTestOnce.Do(func() { 28 | starlarktest.DataFile = func(pkgdir, filename string) string { 29 | _, currFileName, _, ok := runtime.Caller(1) 30 | if !ok { 31 | return "" 32 | } 33 | return filepath.Join(filepath.Dir(currFileName), filename) 34 | } 35 | }) 36 | // for assert loader 37 | return func(thread *starlark.Thread, module string) (starlark.StringDict, error) { 38 | switch module { 39 | case moduleName: 40 | if loader == nil { 41 | return nil, fmt.Errorf("nil module") 42 | } 43 | d, err := loader() 44 | if err != nil { 45 | // failed to load 46 | return nil, err 47 | } 48 | // Aligned with starlet/module.go: GetLazyLoader() function 49 | // extract all members of module from dict like `{name: module}` or `{name: struct}` 50 | if len(d) == 1 { 51 | m, found := d[moduleName] 52 | if found { 53 | if mm, ok := m.(*starlarkstruct.Module); ok && mm != nil { 54 | return mm.Members, nil 55 | } else if sm, ok := m.(*starlarkstruct.Struct); ok && sm != nil { 56 | sd := make(starlark.StringDict) 57 | sm.ToStringDict(sd) 58 | return sd, nil 59 | } 60 | } 61 | } 62 | return d, nil 63 | case "struct.star": 64 | return starlark.StringDict{ 65 | "struct": starlark.NewBuiltin("struct", starlarkstruct.Make), 66 | }, nil 67 | case "module.star": 68 | return starlark.StringDict{ 69 | "module": starlark.NewBuiltin("module", starlarkstruct.MakeModule), 70 | }, nil 71 | case "assert.star": 72 | return starlarktest.LoadAssertModule() 73 | case "freeze.star": 74 | return starlark.StringDict{ 75 | "freeze": starlark.NewBuiltin("freeze", freezeValue), 76 | }, nil 77 | } 78 | 79 | return nil, fmt.Errorf("invalid module") 80 | } 81 | } 82 | 83 | // ExecModuleWithErrorTest executes a Starlark script with a module loader and compares the error with the expected error. 84 | func ExecModuleWithErrorTest(t *testing.T, name string, loader ModuleLoadFunc, script string, wantErr string, predecl starlark.StringDict) (starlark.StringDict, error) { 85 | thread := &starlark.Thread{Load: NewAssertLoader(name, loader), Print: func(_ *starlark.Thread, msg string) { t.Log("※", msg) }} 86 | starlarktest.SetReporter(thread, t) 87 | header := `load('assert.star', 'assert')` 88 | opts := syntax.FileOptions{ 89 | Set: true, 90 | } 91 | out, err := starlark.ExecFileOptions(&opts, thread, name+"_test.star", []byte(header+"\n"+script), predecl) 92 | if err != nil { 93 | if wantErr == "" { 94 | if ee, ok := err.(*starlark.EvalError); ok { 95 | t.Errorf("got unexpected starlark error: '%v'", ee.Backtrace()) 96 | } else { 97 | t.Errorf("got unexpected error: '%v'", err) 98 | } 99 | } else if wantErr != "" && !strings.Contains(err.Error(), wantErr) { 100 | t.Errorf("got mismatched error: '%v', want: '%v'", err, wantErr) 101 | } 102 | } 103 | return out, err 104 | } 105 | 106 | func freezeValue(thread *starlark.Thread, bn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 107 | var v starlark.Value 108 | if err := starlark.UnpackArgs(bn.Name(), args, kwargs, "v", &v); err != nil { 109 | return nil, err 110 | } 111 | v.Freeze() 112 | return v, nil 113 | } 114 | -------------------------------------------------------------------------------- /internal/testloader_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "go.starlark.net/starlark" 8 | "go.starlark.net/starlarkstruct" 9 | ) 10 | 11 | func TestNewAssertLoader(t *testing.T) { 12 | // Dummy ModuleLoadFunc that returns a fixed StringDict 13 | moduleLoadFunc := func() (starlark.StringDict, error) { 14 | return starlark.StringDict{ 15 | "dummy": starlarkstruct.FromStringDict(starlark.String("dummy"), starlark.StringDict{ 16 | "foo": starlark.String("bar"), 17 | }), 18 | }, nil 19 | } 20 | errLoadFunc := func() (starlark.StringDict, error) { 21 | return nil, fmt.Errorf("invalid loader") 22 | } 23 | 24 | tests := []struct { 25 | name string 26 | moduleName string 27 | loadFunc ModuleLoadFunc 28 | expectedError string 29 | }{ 30 | { 31 | name: "Load dummy module", 32 | moduleName: "dummy", 33 | loadFunc: moduleLoadFunc, 34 | }, 35 | { 36 | name: "Load struct.star module", 37 | moduleName: "struct.star", 38 | loadFunc: moduleLoadFunc, 39 | }, 40 | { 41 | name: "Load assert.star module", 42 | moduleName: "assert.star", 43 | loadFunc: moduleLoadFunc, 44 | }, 45 | { 46 | name: "Load freeze.star module", 47 | moduleName: "freeze.star", 48 | loadFunc: moduleLoadFunc, 49 | }, 50 | { 51 | name: "Invalid module", 52 | moduleName: "invalid", 53 | loadFunc: moduleLoadFunc, 54 | expectedError: "invalid module", 55 | }, 56 | { 57 | name: "Nil module", 58 | moduleName: "nil", 59 | loadFunc: nil, 60 | expectedError: "nil module", 61 | }, 62 | { 63 | name: "Error module", 64 | moduleName: "error", 65 | loadFunc: errLoadFunc, 66 | expectedError: "invalid loader", 67 | }, 68 | } 69 | 70 | for _, test := range tests { 71 | t.Run(test.name, func(t *testing.T) { 72 | loader := NewAssertLoader(test.moduleName, test.loadFunc) 73 | thread := &starlark.Thread{Name: "main"} 74 | _, err := loader(thread, test.moduleName) 75 | 76 | if err != nil && err.Error() != test.expectedError { 77 | t.Errorf("expected error %s, got %s", test.expectedError, err) 78 | } 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal_test.go: -------------------------------------------------------------------------------- 1 | package starlet 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | func TestCastStringDictToAnyMap(t *testing.T) { 11 | // Create a starlark.StringDict 12 | m := starlark.StringDict{ 13 | "key1": starlark.String("value1"), 14 | "key2": starlark.String("value2"), 15 | "key3": starlark.NewList([]starlark.Value{starlark.String("value3")}), 16 | } 17 | 18 | // Convert to StringAnyMap 19 | anyMap := castStringDictToAnyMap(m) 20 | 21 | // Check that the conversion was successful 22 | if anyMap["key1"].(starlark.String) != "value1" { 23 | t.Errorf("Expected 'value1', got '%s'", anyMap["key1"].(starlark.String)) 24 | } 25 | if anyMap["key2"].(starlark.String) != "value2" { 26 | t.Errorf("Expected 'value2', got '%s'", anyMap["key2"].(starlark.String)) 27 | } 28 | if !reflect.DeepEqual(anyMap["key3"], m["key3"]) { 29 | t.Errorf("Expected '%v', got '%v'", m["key3"], anyMap["key3"]) 30 | } 31 | } 32 | 33 | func TestCastStringAnyMapToStringDict(t *testing.T) { 34 | // Create a StringAnyMap 35 | m := StringAnyMap{ 36 | "key1": starlark.String("value1"), 37 | "key2": starlark.String("value2"), 38 | "key3": starlark.NewList([]starlark.Value{starlark.String("value3")}), 39 | } 40 | 41 | // Convert to starlark.StringDict 42 | stringDict, err := castStringAnyMapToStringDict(m) 43 | 44 | // Check that the conversion was successful 45 | if err != nil { 46 | t.Errorf("Unexpected error: %v", err) 47 | } 48 | if stringDict["key1"].(starlark.String) != "value1" { 49 | t.Errorf("Expected 'value1', got '%s'", stringDict["key1"].(starlark.String)) 50 | } 51 | if stringDict["key2"].(starlark.String) != "value2" { 52 | t.Errorf("Expected 'value2', got '%s'", stringDict["key2"].(starlark.String)) 53 | } 54 | if !reflect.DeepEqual(stringDict["key3"], m["key3"]) { 55 | t.Errorf("Expected '%v', got '%v'", m["key3"], stringDict["key3"]) 56 | } 57 | 58 | // Test with a non-starlark.Value in the map 59 | m["key4"] = "value4" 60 | _, err = castStringAnyMapToStringDict(m) 61 | 62 | // Check that an error was returned 63 | if err == nil { 64 | t.Errorf("Expected error, got nil") 65 | } 66 | } 67 | 68 | func TestSetInputConversionEnabled(t *testing.T) { 69 | m := NewDefault() 70 | if m.enableInConv != true { 71 | t.Errorf("Expected input conversion to be enabled by default, but it wasn't") 72 | } 73 | 74 | m.SetInputConversionEnabled(false) 75 | if m.enableInConv != false { 76 | t.Errorf("Expected input conversion to be disabled, but it wasn't") 77 | } 78 | 79 | m.SetInputConversionEnabled(true) 80 | if m.enableInConv != true { 81 | t.Errorf("Expected input conversion to be enabled, but it wasn't") 82 | } 83 | } 84 | 85 | func TestSetOutputConversionEnabled(t *testing.T) { 86 | m := NewDefault() 87 | if m.enableOutConv != true { 88 | t.Errorf("Expected output conversion to be enabled by default, but it wasn't") 89 | } 90 | 91 | m.SetOutputConversionEnabled(false) 92 | if m.enableOutConv != false { 93 | t.Errorf("Expected output conversion to be disabled, but it wasn't") 94 | } 95 | 96 | m.SetOutputConversionEnabled(true) 97 | if m.enableOutConv != true { 98 | t.Errorf("Expected output conversion to be enabled, but it wasn't") 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/atom/README.md: -------------------------------------------------------------------------------- 1 | # atom 2 | 3 | atom provides atomic operations for integers, floats, and strings. 4 | 5 | ## Functions 6 | 7 | ### `new_int(value=0) -> AtomicInt` 8 | 9 | create a new AtomicInt with an optional initial value 10 | 11 | #### Parameters 12 | 13 | | name | type | description | 14 | |---------|-------|------------------------------| 15 | | `value` | `int` | initial value, defaults to 0 | 16 | 17 | #### Examples 18 | 19 | **basic** 20 | 21 | create a new AtomicInt with default value 22 | 23 | ```python 24 | load("atom", "new_int") 25 | ai = new_int() 26 | ai.inc() 27 | print(ai.get()) 28 | # Output: 1 29 | ``` 30 | 31 | **with value** 32 | 33 | create a new AtomicInt with a specific value 34 | 35 | ```python 36 | load("atom", "new_int") 37 | ai = new_int(42) 38 | ai.add(42) 39 | print(ai.get()) 40 | # Output: 84 41 | ``` 42 | 43 | ### `new_float(value=0.0) -> AtomicFloat` 44 | 45 | create a new AtomicFloat with an optional initial value 46 | 47 | #### Parameters 48 | 49 | | name | type | description | 50 | |---------|---------|--------------------------------| 51 | | `value` | `float` | initial value, defaults to 0.0 | 52 | 53 | #### Examples 54 | 55 | **basic** 56 | 57 | create a new AtomicFloat with default value 58 | 59 | ```python 60 | load("atom", "new_float") 61 | af = new_float() 62 | print(af.get()) 63 | # Output: 0.0 64 | ``` 65 | 66 | **with value** 67 | 68 | create a new AtomicFloat with a specific value 69 | 70 | ```python 71 | load("atom", "new_float") 72 | af = new_float(3.14) 73 | print(af.get()) 74 | # Output: 3.14 75 | ``` 76 | 77 | ### `new_string(value="") -> AtomicString` 78 | 79 | create a new AtomicString with an optional initial value 80 | 81 | #### Parameters 82 | 83 | | name | type | description | 84 | |---------|----------|--------------------------------------------| 85 | | `value` | `string` | initial value, defaults to an empty string | 86 | 87 | #### Examples 88 | 89 | **basic** 90 | 91 | create a new AtomicString with default value 92 | 93 | ```python 94 | load("atom", "new_string") 95 | as = new_string() 96 | print(as.get()) # Output: "" 97 | ``` 98 | 99 | **with value** 100 | 101 | create a new AtomicString with a specific value 102 | 103 | ```python 104 | load("atom", "new_string") 105 | as = new_string("hello") 106 | print(as.get()) 107 | # Output: "hello" 108 | ``` 109 | 110 | ## Types 111 | 112 | ### `AtomicInt` 113 | 114 | an atomic integer type with various atomic operations 115 | 116 | **Methods** 117 | 118 | #### `get() -> int` 119 | 120 | returns the current value 121 | 122 | #### `set(value: int)` 123 | 124 | sets the value 125 | 126 | #### `cas(old: int, new: int) -> bool` 127 | 128 | compares and swaps the value if it matches old 129 | 130 | #### `add(delta: int) -> int` 131 | 132 | adds delta to the value and returns the new value 133 | 134 | #### `sub(delta: int) -> int` 135 | 136 | subtracts delta from the value and returns the new value 137 | 138 | #### `inc() -> int` 139 | 140 | increments the value by 1 and returns the new value 141 | 142 | #### `dec() -> int` 143 | 144 | decrements the value by 1 and returns the new value 145 | 146 | ### `AtomicFloat` 147 | 148 | an atomic float type with various atomic operations 149 | 150 | **Methods** 151 | 152 | #### `get() -> float` 153 | 154 | returns the current value 155 | 156 | #### `set(value: float)` 157 | 158 | sets the value 159 | 160 | #### `cas(old: float, new: float) -> bool` 161 | 162 | compares and swaps the value if it matches old 163 | 164 | #### `add(delta: float) -> float` 165 | 166 | adds delta to the value and returns the new value 167 | 168 | #### `sub(delta: float) -> float` 169 | 170 | subtracts delta from the value and returns the new value 171 | 172 | ### `AtomicString` 173 | 174 | an atomic string type with various atomic operations 175 | 176 | **Methods** 177 | 178 | #### `get() -> string` 179 | 180 | returns the current value 181 | 182 | #### `set(value: string)` 183 | 184 | sets the value 185 | 186 | #### `cas(old: string, new: string) -> bool` 187 | 188 | compares and swaps the value if it matches old 189 | -------------------------------------------------------------------------------- /lib/atom/atom.go: -------------------------------------------------------------------------------- 1 | // Package atom provides atomic operations for integers, floats and strings. 2 | // Inspired by the sync/atomic and go.uber.org/atomic packages from Go. 3 | package atom 4 | 5 | import ( 6 | "fmt" 7 | "strings" 8 | "sync" 9 | 10 | tps "github.com/1set/starlet/dataconv/types" 11 | "go.starlark.net/starlark" 12 | "go.starlark.net/starlarkstruct" 13 | "go.starlark.net/syntax" 14 | "go.uber.org/atomic" 15 | ) 16 | 17 | // ModuleName defines the expected name for this Module when used in starlark's load() function, eg: load('atom', 'new_int') 18 | const ModuleName = "atom" 19 | 20 | var ( 21 | once sync.Once 22 | atomModule starlark.StringDict 23 | ) 24 | 25 | // LoadModule loads the atom module. It is concurrency-safe and idempotent. 26 | func LoadModule() (starlark.StringDict, error) { 27 | once.Do(func() { 28 | atomModule = starlark.StringDict{ 29 | ModuleName: &starlarkstruct.Module{ 30 | Name: ModuleName, 31 | Members: starlark.StringDict{ 32 | "new_int": starlark.NewBuiltin(ModuleName+".new_int", newInt), 33 | "new_float": starlark.NewBuiltin(ModuleName+".new_float", newFloat), 34 | "new_string": starlark.NewBuiltin(ModuleName+".new_string", newString), 35 | }, 36 | }, 37 | } 38 | }) 39 | return atomModule, nil 40 | } 41 | 42 | // for integer 43 | 44 | var ( 45 | _ starlark.Value = (*AtomicInt)(nil) 46 | _ starlark.HasAttrs = (*AtomicInt)(nil) 47 | _ starlark.Comparable = (*AtomicInt)(nil) 48 | ) 49 | 50 | func newInt(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 51 | var value int64 52 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "value?", &value); err != nil { 53 | return nil, err 54 | } 55 | return &AtomicInt{val: atomic.NewInt64(value)}, nil 56 | } 57 | 58 | type AtomicInt struct { 59 | val *atomic.Int64 60 | frozen bool 61 | } 62 | 63 | func (a *AtomicInt) String() string { 64 | return fmt.Sprintf("", a.val.Load()) 65 | } 66 | 67 | func (a *AtomicInt) Type() string { 68 | return "atom_int" 69 | } 70 | 71 | func (a *AtomicInt) Freeze() { 72 | a.frozen = true 73 | } 74 | 75 | func (a *AtomicInt) Truth() starlark.Bool { 76 | return a.val.Load() != 0 77 | } 78 | 79 | func (a *AtomicInt) Hash() (uint32, error) { 80 | //return 0, fmt.Errorf("unhashable: %s", a.Type()) 81 | return hashInt64(a.val.Load()), nil 82 | } 83 | 84 | func (a *AtomicInt) Attr(name string) (starlark.Value, error) { 85 | return builtinAttr(a, name, intMethods) 86 | } 87 | 88 | func (a *AtomicInt) AttrNames() []string { 89 | return builtinAttrNames(intMethods) 90 | } 91 | 92 | func (a *AtomicInt) CompareSameType(op syntax.Token, y_ starlark.Value, depth int) (bool, error) { 93 | vx := a.val.Load() 94 | y := y_.(*AtomicInt) 95 | vy := y.val.Load() 96 | 97 | cmp := 0 98 | if vx < vy { 99 | cmp = -1 100 | } else if vx > vy { 101 | cmp = 1 102 | } else { 103 | cmp = 0 104 | } 105 | return threewayCompare(op, cmp) 106 | } 107 | 108 | // for float 109 | 110 | var ( 111 | _ starlark.Value = (*AtomicFloat)(nil) 112 | _ starlark.HasAttrs = (*AtomicFloat)(nil) 113 | _ starlark.Comparable = (*AtomicFloat)(nil) 114 | ) 115 | 116 | type AtomicFloat struct { 117 | val *atomic.Float64 118 | frozen bool 119 | } 120 | 121 | func newFloat(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 122 | var value tps.FloatOrInt 123 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "value?", &value); err != nil { 124 | return nil, err 125 | } 126 | return &AtomicFloat{val: atomic.NewFloat64(value.GoFloat())}, nil 127 | } 128 | 129 | func (a *AtomicFloat) String() string { 130 | return fmt.Sprintf("", a.val.Load()) 131 | } 132 | 133 | func (a *AtomicFloat) Type() string { 134 | return "atom_float" 135 | } 136 | 137 | func (a *AtomicFloat) Freeze() { 138 | a.frozen = true 139 | } 140 | 141 | func (a *AtomicFloat) Truth() starlark.Bool { 142 | return a.val.Load() != 0 143 | } 144 | 145 | func (a *AtomicFloat) Hash() (uint32, error) { 146 | return hashFloat64(a.val.Load()), nil 147 | } 148 | 149 | func (a *AtomicFloat) Attr(name string) (starlark.Value, error) { 150 | return builtinAttr(a, name, floatMethods) 151 | } 152 | 153 | func (a *AtomicFloat) AttrNames() []string { 154 | return builtinAttrNames(floatMethods) 155 | } 156 | 157 | func (a *AtomicFloat) CompareSameType(op syntax.Token, y_ starlark.Value, depth int) (bool, error) { 158 | vx := a.val.Load() 159 | y := y_.(*AtomicFloat) 160 | vy := y.val.Load() 161 | 162 | cmp := 0 163 | if vx < vy { 164 | cmp = -1 165 | } else if vx > vy { 166 | cmp = 1 167 | } else { 168 | cmp = 0 169 | } 170 | return threewayCompare(op, cmp) 171 | } 172 | 173 | // for string 174 | 175 | var ( 176 | _ starlark.Value = (*AtomicString)(nil) 177 | _ starlark.HasAttrs = (*AtomicString)(nil) 178 | _ starlark.Comparable = (*AtomicString)(nil) 179 | ) 180 | 181 | type AtomicString struct { 182 | val *atomic.String 183 | frozen bool 184 | } 185 | 186 | func newString(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 187 | var value string 188 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "value?", &value); err != nil { 189 | return nil, err 190 | } 191 | return &AtomicString{val: atomic.NewString(value)}, nil 192 | } 193 | 194 | func (a *AtomicString) String() string { 195 | return fmt.Sprintf("", a.val.Load()) 196 | } 197 | 198 | func (a *AtomicString) Type() string { 199 | return "atom_string" 200 | } 201 | 202 | func (a *AtomicString) Freeze() { 203 | a.frozen = true 204 | } 205 | 206 | func (a *AtomicString) Truth() starlark.Bool { 207 | return a.val.Load() != "" 208 | } 209 | 210 | func (a *AtomicString) Hash() (uint32, error) { 211 | return hashString(a.val.Load()), nil 212 | } 213 | 214 | func (a *AtomicString) Attr(name string) (starlark.Value, error) { 215 | return builtinAttr(a, name, stringMethods) 216 | } 217 | 218 | func (a *AtomicString) AttrNames() []string { 219 | return builtinAttrNames(stringMethods) 220 | } 221 | 222 | func (a *AtomicString) CompareSameType(op syntax.Token, y_ starlark.Value, depth int) (bool, error) { 223 | vx := a.val.Load() 224 | y := y_.(*AtomicString) 225 | vy := y.val.Load() 226 | 227 | return threewayCompare(op, strings.Compare(vx, vy)) 228 | } 229 | -------------------------------------------------------------------------------- /lib/atom/helper.go: -------------------------------------------------------------------------------- 1 | package atom 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "hash/fnv" 7 | "math" 8 | "sort" 9 | 10 | "go.starlark.net/starlark" 11 | "go.starlark.net/syntax" 12 | ) 13 | 14 | func builtinAttr(recv starlark.Value, name string, methods map[string]*starlark.Builtin) (starlark.Value, error) { 15 | b := methods[name] 16 | if b == nil { 17 | return nil, nil // no such method 18 | } 19 | return b.BindReceiver(recv), nil 20 | } 21 | 22 | func builtinAttrNames(methods map[string]*starlark.Builtin) []string { 23 | names := make([]string, 0, len(methods)) 24 | for name := range methods { 25 | names = append(names, name) 26 | } 27 | sort.Strings(names) 28 | return names 29 | } 30 | 31 | // hashInt64 hashes an int64 value to a uint32 hash value using little-endian byte order 32 | func hashInt64(value int64) uint32 { 33 | // Allocate a byte slice 34 | bytes := make([]byte, 8) 35 | // Convert the int64 value into bytes using little-endian encoding 36 | binary.LittleEndian.PutUint64(bytes, uint64(value)) 37 | // Initialize a new 32-bit FNV-1a hash 38 | h := fnv.New32a() 39 | // Write the bytes to the hasher, and ignore the error returned by Write, as hashing can't really fail here 40 | _, _ = h.Write(bytes) 41 | // Calculate the hash and return it 42 | return h.Sum32() 43 | } 44 | 45 | // hashFloat64 hashes a float64 value to a uint32 hash value 46 | func hashFloat64(value float64) uint32 { 47 | // Convert the float64 value into its binary representation as uint64 48 | bits := math.Float64bits(value) 49 | // Allocate a byte slice 50 | bytes := make([]byte, 8) 51 | // Convert the uint64 bits into bytes using little-endian encoding 52 | binary.LittleEndian.PutUint64(bytes, bits) 53 | // Initialize a new 32-bit FNV-1a hash 54 | h := fnv.New32a() 55 | // Write the bytes to the hasher, and ignore the error returned by Write, as hashing can't really fail here 56 | _, _ = h.Write(bytes) 57 | // Calculate the hash and return it 58 | return h.Sum32() 59 | } 60 | 61 | // hashString hashes a string value to a uint32 hash value 62 | func hashString(value string) uint32 { 63 | // Initialize a new 32-bit FNV-1a hash 64 | h := fnv.New32a() 65 | // Write the string to the hasher, and ignore the error returned by Write, as hashing can't really fail here 66 | _, _ = h.Write([]byte(value)) 67 | // Calculate the hash and return it 68 | return h.Sum32() 69 | } 70 | 71 | // threewayCompare interprets a three-way comparison value cmp (-1, 0, +1) 72 | // as a boolean comparison (e.g. x < y). 73 | func threewayCompare(op syntax.Token, cmp int) (bool, error) { 74 | switch op { 75 | case syntax.EQL: 76 | return cmp == 0, nil 77 | case syntax.NEQ: 78 | return cmp != 0, nil 79 | case syntax.LE: 80 | return cmp <= 0, nil 81 | case syntax.LT: 82 | return cmp < 0, nil 83 | case syntax.GE: 84 | return cmp >= 0, nil 85 | case syntax.GT: 86 | return cmp > 0, nil 87 | default: 88 | return false, fmt.Errorf("unexpected comparison operator %s", op) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/atom/helper_test.go: -------------------------------------------------------------------------------- 1 | package atom 2 | 3 | import ( 4 | "math" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func Test_hashInt64(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | value int64 13 | want uint32 14 | }{ 15 | { 16 | name: "zero", 17 | value: 0, 18 | want: 2615243109, 19 | }, 20 | { 21 | name: "+1", 22 | value: 1, 23 | want: 1048580676, 24 | }, 25 | { 26 | name: "-1", 27 | value: -1, 28 | want: 1823345245, 29 | }, 30 | { 31 | name: "max", 32 | value: math.MaxInt64, 33 | want: 3970880477, 34 | }, 35 | } 36 | for _, tt := range tests { 37 | t.Run(tt.name, func(t *testing.T) { 38 | if got := hashInt64(tt.value); got != tt.want { 39 | t.Errorf("hashInt64(%d) = %v, want %v", tt.value, got, tt.want) 40 | } 41 | }) 42 | } 43 | } 44 | 45 | func Test_hashFloat64(t *testing.T) { 46 | tests := []struct { 47 | name string 48 | value float64 49 | want uint32 50 | }{ 51 | { 52 | name: "zero", 53 | value: 0, 54 | want: 2615243109, 55 | }, 56 | { 57 | name: "+1", 58 | value: 1, 59 | want: 2355796088, 60 | }, 61 | { 62 | name: "-1", 63 | value: -1, 64 | want: 208260856, 65 | }, 66 | { 67 | name: "max", 68 | value: math.MaxFloat64, 69 | want: 3968320621, 70 | }, 71 | } 72 | for _, tt := range tests { 73 | t.Run(tt.name, func(t *testing.T) { 74 | if got := hashFloat64(tt.value); got != tt.want { 75 | t.Errorf("hashFloat64(%f) = %v, want %v", tt.value, got, tt.want) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func Test_hashString(t *testing.T) { 82 | tests := []struct { 83 | name string 84 | input string 85 | want uint32 86 | }{ 87 | { 88 | name: "empty", 89 | input: "", 90 | want: 2166136261, 91 | }, 92 | { 93 | name: "single", 94 | input: "a", 95 | want: 3826002220, 96 | }, 97 | { 98 | name: "next", 99 | input: "b", 100 | want: 3876335077, 101 | }, 102 | { 103 | name: "add", 104 | input: "ab", 105 | want: 1294271946, 106 | }, 107 | { 108 | name: "hello", 109 | input: "hello", 110 | want: 1335831723, 111 | }, 112 | { 113 | name: "long", 114 | input: strings.Repeat("this is a long string", 100), 115 | want: 229378413, 116 | }, 117 | } 118 | for _, tt := range tests { 119 | got := hashString(tt.input) 120 | if got != tt.want { 121 | t.Errorf("hashString(%q) = %v, want %v", tt.input, got, tt.want) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/atom/method.go: -------------------------------------------------------------------------------- 1 | package atom 2 | 3 | import ( 4 | tps "github.com/1set/starlet/dataconv/types" 5 | "go.starlark.net/starlark" 6 | ) 7 | 8 | // for integer 9 | 10 | var ( 11 | intMethods = map[string]*starlark.Builtin{ 12 | "get": starlark.NewBuiltin("get", intGet), 13 | "set": starlark.NewBuiltin("set", intSet), 14 | "cas": starlark.NewBuiltin("cas", intCAS), 15 | "add": starlark.NewBuiltin("add", intAdd), 16 | "sub": starlark.NewBuiltin("sub", intSub), 17 | "inc": starlark.NewBuiltin("inc", intInc), 18 | "dec": starlark.NewBuiltin("dec", intDec), 19 | } 20 | ) 21 | 22 | func intGet(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 23 | if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 0); err != nil { 24 | return nil, err 25 | } 26 | recv := b.Receiver().(*AtomicInt) 27 | return starlark.MakeInt64(recv.val.Load()), nil 28 | } 29 | 30 | func intSet(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 31 | var value int64 32 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "value", &value); err != nil { 33 | return nil, err 34 | } 35 | recv := b.Receiver().(*AtomicInt) 36 | recv.val.Store(value) 37 | return starlark.None, nil 38 | } 39 | 40 | func intCAS(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 41 | var oldVal, newVal int64 42 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "old", &oldVal, "new", &newVal); err != nil { 43 | return nil, err 44 | } 45 | recv := b.Receiver().(*AtomicInt) 46 | return starlark.Bool(recv.val.CAS(oldVal, newVal)), nil 47 | } 48 | 49 | func intAdd(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 50 | var delta int64 51 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "delta", &delta); err != nil { 52 | return nil, err 53 | } 54 | recv := b.Receiver().(*AtomicInt) 55 | return starlark.MakeInt64(recv.val.Add(delta)), nil 56 | } 57 | 58 | func intSub(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 59 | var delta int64 60 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "delta", &delta); err != nil { 61 | return nil, err 62 | } 63 | recv := b.Receiver().(*AtomicInt) 64 | return starlark.MakeInt64(recv.val.Sub(delta)), nil 65 | } 66 | 67 | func intInc(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 68 | if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 0); err != nil { 69 | return nil, err 70 | } 71 | recv := b.Receiver().(*AtomicInt) 72 | return starlark.MakeInt64(recv.val.Inc()), nil 73 | } 74 | 75 | func intDec(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 76 | if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 0); err != nil { 77 | return nil, err 78 | } 79 | recv := b.Receiver().(*AtomicInt) 80 | return starlark.MakeInt64(recv.val.Dec()), nil 81 | } 82 | 83 | // for float 84 | 85 | var ( 86 | floatMethods = map[string]*starlark.Builtin{ 87 | "get": starlark.NewBuiltin("get", floatGet), 88 | "set": starlark.NewBuiltin("set", floatSet), 89 | "cas": starlark.NewBuiltin("cas", floatCAS), 90 | "add": starlark.NewBuiltin("add", floatAdd), 91 | "sub": starlark.NewBuiltin("sub", floatSub), 92 | } 93 | ) 94 | 95 | func floatGet(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 96 | if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 0); err != nil { 97 | return nil, err 98 | } 99 | recv := b.Receiver().(*AtomicFloat) 100 | return starlark.Float(recv.val.Load()), nil 101 | } 102 | 103 | func floatSet(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 104 | var value tps.FloatOrInt 105 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "value", &value); err != nil { 106 | return nil, err 107 | } 108 | recv := b.Receiver().(*AtomicFloat) 109 | recv.val.Store(value.GoFloat()) 110 | return starlark.None, nil 111 | } 112 | 113 | func floatCAS(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 114 | var oldVal, newVal tps.FloatOrInt 115 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "old", &oldVal, "new", &newVal); err != nil { 116 | return nil, err 117 | } 118 | recv := b.Receiver().(*AtomicFloat) 119 | return starlark.Bool(recv.val.CAS(oldVal.GoFloat(), newVal.GoFloat())), nil 120 | } 121 | 122 | func floatAdd(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 123 | var delta tps.FloatOrInt 124 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "delta", &delta); err != nil { 125 | return nil, err 126 | } 127 | recv := b.Receiver().(*AtomicFloat) 128 | return starlark.Float(recv.val.Add(delta.GoFloat())), nil 129 | } 130 | 131 | func floatSub(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 132 | var delta tps.FloatOrInt 133 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "delta", &delta); err != nil { 134 | return nil, err 135 | } 136 | recv := b.Receiver().(*AtomicFloat) 137 | return starlark.Float(recv.val.Sub(delta.GoFloat())), nil 138 | } 139 | 140 | // for string 141 | 142 | var ( 143 | stringMethods = map[string]*starlark.Builtin{ 144 | "get": starlark.NewBuiltin("get", stringGet), 145 | "set": starlark.NewBuiltin("set", stringSet), 146 | "cas": starlark.NewBuiltin("cas", stringCAS), 147 | } 148 | ) 149 | 150 | func stringGet(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 151 | if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 0); err != nil { 152 | return nil, err 153 | } 154 | recv := b.Receiver().(*AtomicString) 155 | return starlark.String(recv.val.Load()), nil 156 | } 157 | 158 | func stringSet(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 159 | var value string 160 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "value", &value); err != nil { 161 | return nil, err 162 | } 163 | recv := b.Receiver().(*AtomicString) 164 | recv.val.Store(value) 165 | return starlark.None, nil 166 | } 167 | 168 | func stringCAS(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 169 | var oldVal, newVal string 170 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "old", &oldVal, "new", &newVal); err != nil { 171 | return nil, err 172 | } 173 | recv := b.Receiver().(*AtomicString) 174 | return starlark.Bool(recv.val.CompareAndSwap(oldVal, newVal)), nil 175 | } 176 | -------------------------------------------------------------------------------- /lib/base64/README.md: -------------------------------------------------------------------------------- 1 | # base64 2 | 3 | `base64` defines base64 encoding & decoding functions, often used to represent binary as text. 4 | 5 | ## Functions 6 | 7 | ### `encode(src,encoding="standard") string` 8 | 9 | return the base64 encoding of src 10 | 11 | #### Parameters 12 | 13 | | name | type | description | 14 | |------------|----------|-------------------------------------------------------------------------------------------------| 15 | | `src` | `string` | source string to encode to base64 | 16 | | `encoding` | `string` | optional. string to set encoding dialect. allowed values are: standard,standard_raw,url,url_raw | 17 | 18 | #### Examples 19 | 20 | **basic** 21 | 22 | encode a string as base64 23 | 24 | ```python 25 | load("base64", "encode") 26 | encoded = encode("hello world!") 27 | print(encoded) 28 | # Output: aGVsbG8gd29ybGQh 29 | ``` 30 | 31 | ### `decode(src,encoding="standard") string` 32 | 33 | parse base64 input, giving back the plain string representation 34 | 35 | #### Parameters 36 | 37 | | name | type | description | 38 | |------------|----------|-------------------------------------------------------------------------------------------------| 39 | | `src` | `string` | source string of base64-encoded text | 40 | | `encoding` | `string` | optional. string to set decoding dialect. allowed values are: standard,standard_raw,url,url_raw | 41 | 42 | #### Examples 43 | 44 | **basic** 45 | 46 | encode a string as base64 47 | 48 | ```python 49 | load("base64", "decode") 50 | decoded = decode("aGVsbG8gd29ybGQh") 51 | print(decoded) 52 | # Output: hello world! 53 | ``` 54 | -------------------------------------------------------------------------------- /lib/base64/base64.go: -------------------------------------------------------------------------------- 1 | // Package base64 defines base64 encoding & decoding functions for Starlark. 2 | // 3 | // Migrated from: https://github.com/qri-io/starlib/tree/master/encoding/base64 4 | package base64 5 | 6 | import ( 7 | gobase64 "encoding/base64" 8 | "fmt" 9 | "sync" 10 | 11 | tps "github.com/1set/starlet/dataconv/types" 12 | "go.starlark.net/starlark" 13 | "go.starlark.net/starlarkstruct" 14 | ) 15 | 16 | // ModuleName defines the expected name for this Module when used in starlark's load() function, eg: load('base64', 'encode') 17 | const ModuleName = "base64" 18 | 19 | var ( 20 | once sync.Once 21 | base64Module starlark.StringDict 22 | ) 23 | 24 | // Encodings is a map of strings to encoding formats. It is used to select the encoding format for the base64 module. 25 | // You can add your own encoding formats to this map. 26 | var Encodings = map[string]*gobase64.Encoding{ 27 | // StdEncoding is the standard base64 encoding, as defined in RFC 4648. 28 | "standard": gobase64.StdEncoding, 29 | // RawStdEncoding is the standard raw, unpadded base64 encoding, 30 | // as defined in RFC 4648 section 3.2. 31 | // This is the same as StdEncoding but omits padding characters. 32 | "standard_raw": gobase64.RawStdEncoding, 33 | // URLEncoding is the alternate base64 encoding defined in RFC 4648. 34 | // It is typically used in URLs and file names. 35 | "url": gobase64.URLEncoding, 36 | // RawURLEncoding is the unpadded alternate base64 encoding defined in RFC 4648. 37 | // It is typically used in URLs and file names. 38 | // This is the same as URLEncoding but omits padding characters. 39 | "url_raw": gobase64.RawURLEncoding, 40 | } 41 | 42 | // LoadModule loads the base64 module. 43 | // It is concurrency-safe and idempotent. 44 | func LoadModule() (starlark.StringDict, error) { 45 | once.Do(func() { 46 | base64Module = starlark.StringDict{ 47 | "base64": &starlarkstruct.Module{ 48 | Name: "base64", 49 | Members: starlark.StringDict{ 50 | "encode": starlark.NewBuiltin("base64.encode", encodeString), 51 | "decode": starlark.NewBuiltin("base64.decode", decodeString), 52 | }, 53 | }, 54 | } 55 | }) 56 | return base64Module, nil 57 | } 58 | 59 | func selectEncoder(encoding starlark.String) (encoder *gobase64.Encoding, err error) { 60 | if encoding == "" { 61 | encoding = "standard" 62 | } 63 | encoder = Encodings[string(encoding)] 64 | if encoder == nil { 65 | err = fmt.Errorf("unsupported encoding format: %s", encoding) 66 | } 67 | return 68 | } 69 | 70 | func encodeString(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 71 | var ( 72 | data tps.StringOrBytes 73 | encoding starlark.String 74 | ) 75 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "data", &data, "encoding?", &encoding); err != nil { 76 | return starlark.None, err 77 | } 78 | 79 | encoder, err := selectEncoder(encoding) 80 | if err != nil { 81 | return starlark.None, err 82 | } 83 | 84 | enc := encoder.EncodeToString([]byte(data)) 85 | return starlark.String(enc), nil 86 | } 87 | 88 | func decodeString(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 89 | var ( 90 | data tps.StringOrBytes 91 | encoding starlark.String 92 | ) 93 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "data", &data, "encoding?", &encoding); err != nil { 94 | return starlark.None, err 95 | } 96 | 97 | encoder, err := selectEncoder(encoding) 98 | if err != nil { 99 | return starlark.None, err 100 | } 101 | 102 | dec, err := encoder.DecodeString(string(data)) 103 | if err != nil { 104 | return starlark.None, err 105 | } 106 | return starlark.String(dec), nil 107 | } 108 | -------------------------------------------------------------------------------- /lib/base64/base64_test.go: -------------------------------------------------------------------------------- 1 | package base64_test 2 | 3 | import ( 4 | "testing" 5 | 6 | itn "github.com/1set/starlet/internal" 7 | "github.com/1set/starlet/lib/base64" 8 | ) 9 | 10 | func TestLoadModule_Base64(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | script string 14 | wantErr string 15 | }{ 16 | { 17 | name: `encode`, 18 | script: itn.HereDoc(` 19 | load('base64', 'encode') 20 | assert.eq(encode("hello"), "aGVsbG8=") 21 | assert.eq(encode("hello", encoding="standard_raw"), "aGVsbG8") 22 | assert.eq(encode("hello friend!", encoding="url"), "aGVsbG8gZnJpZW5kIQ==") 23 | assert.eq(encode("hello friend!", encoding="url_raw"), "aGVsbG8gZnJpZW5kIQ") 24 | `), 25 | }, 26 | { 27 | name: `encode with invalid encoding`, 28 | script: itn.HereDoc(` 29 | load('base64', 'encode') 30 | encode("hello", encoding="invalid") 31 | `), 32 | wantErr: `unsupported encoding format: "invalid"`, 33 | }, 34 | { 35 | name: `encode with invalid input`, 36 | script: itn.HereDoc(` 37 | load('base64', 'encode') 38 | encode(123) 39 | `), 40 | wantErr: `base64.encode: for parameter data: got int, want string or bytes`, 41 | }, 42 | { 43 | name: `decode`, 44 | script: itn.HereDoc(` 45 | load('base64', 'decode') 46 | assert.eq(decode("aGVsbG8="),"hello") 47 | assert.eq(decode("aGVsbG8", encoding="standard_raw"),"hello") 48 | assert.eq(decode("aGVsbG8gZnJpZW5kIQ==", encoding="url"),"hello friend!") 49 | assert.eq(decode("aGVsbG8gZnJpZW5kIQ", encoding="url_raw"),"hello friend!") 50 | `), 51 | }, 52 | { 53 | name: `decode with invalid encoding`, 54 | script: itn.HereDoc(` 55 | load('base64', 'decode') 56 | decode("aGVsbG8=", encoding="invalid") 57 | `), 58 | wantErr: `unsupported encoding format: "invalid"`, 59 | }, 60 | { 61 | name: `decode with invalid input`, 62 | script: itn.HereDoc(` 63 | load('base64', 'decode') 64 | decode(123) 65 | `), 66 | wantErr: `base64.decode: for parameter data: got int, want string or bytes`, 67 | }, 68 | { 69 | name: `decode fail`, 70 | script: itn.HereDoc(` 71 | load('base64', 'decode') 72 | decode("aGVsbG8") 73 | `), 74 | wantErr: `illegal base64 data at input byte 4`, 75 | }, 76 | } 77 | for _, tt := range tests { 78 | t.Run(tt.name, func(t *testing.T) { 79 | res, err := itn.ExecModuleWithErrorTest(t, base64.ModuleName, base64.LoadModule, tt.script, tt.wantErr, nil) 80 | if (err != nil) != (tt.wantErr != "") { 81 | t.Errorf("base64(%q) expects error = '%v', actual error = '%v', result = %v", tt.name, tt.wantErr, err, res) 82 | return 83 | } 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/csv/csv.go: -------------------------------------------------------------------------------- 1 | // Package csv reads comma-separated values from strings and writes CSV data to strings. 2 | // 3 | // Migrated from https://github.com/qri-io/starlib/tree/master/encoding/csv 4 | package csv 5 | 6 | import ( 7 | "bytes" 8 | "encoding/csv" 9 | "fmt" 10 | "sync" 11 | 12 | "github.com/1set/starlet/dataconv" 13 | tps "github.com/1set/starlet/dataconv/types" 14 | "github.com/1set/starlet/internal/replacecr" 15 | "github.com/1set/starlet/lib/file" 16 | "go.starlark.net/starlark" 17 | "go.starlark.net/starlarkstruct" 18 | ) 19 | 20 | // ModuleName defines the expected name for this Module when used in starlark's load() function, eg: load('csv', 'read_all') 21 | const ModuleName = "csv" 22 | 23 | var ( 24 | once sync.Once 25 | csvModule starlark.StringDict 26 | ) 27 | 28 | // LoadModule loads the base64 module. 29 | // It is concurrency-safe and idempotent. 30 | func LoadModule() (starlark.StringDict, error) { 31 | once.Do(func() { 32 | csvModule = starlark.StringDict{ 33 | ModuleName: &starlarkstruct.Module{ 34 | Name: ModuleName, 35 | Members: starlark.StringDict{ 36 | "read_all": starlark.NewBuiltin(ModuleName+".read_all", readAll), 37 | "write_all": starlark.NewBuiltin(ModuleName+".write_all", writeAll), 38 | "write_dict": starlark.NewBuiltin(ModuleName+".write_dict", writeDict), 39 | }, 40 | }, 41 | } 42 | }) 43 | return csvModule, nil 44 | } 45 | 46 | // readAll gets all values from a csv source string. 47 | func readAll(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 48 | var ( 49 | source tps.StringOrBytes 50 | lazyQuotes, trimLeadingSpace bool 51 | skipRow, limitRow int 52 | fieldsPerRecord int 53 | _comma, _comment starlark.String 54 | ) 55 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, 56 | "source", &source, 57 | "comma?", &_comma, 58 | "comment", &_comment, 59 | "lazy_quotes", &lazyQuotes, 60 | "trim_leading_space", &trimLeadingSpace, 61 | "fields_per_record?", &fieldsPerRecord, 62 | "skip?", &skipRow, 63 | "limit?", &limitRow); err != nil { 64 | return nil, err 65 | } 66 | 67 | // prepare reader 68 | rawStr := file.TrimUTF8BOM([]byte(source.GoString())) 69 | csvr := csv.NewReader(replacecr.Reader(bytes.NewReader(rawStr))) 70 | csvr.LazyQuotes = lazyQuotes 71 | csvr.TrimLeadingSpace = trimLeadingSpace 72 | 73 | comma := string(_comma) 74 | if comma == "" { 75 | comma = "," 76 | } else if len(comma) != 1 { 77 | return starlark.None, fmt.Errorf("%s: expected comma param to be a single-character string", b.Name()) 78 | } 79 | csvr.Comma = []rune(comma)[0] 80 | 81 | comment := string(_comment) 82 | if comment != "" && len(comment) != 1 { 83 | return starlark.None, fmt.Errorf("%s: expected comment param to be a single-character string", b.Name()) 84 | } else if comment != "" { 85 | csvr.Comment = []rune(comment)[0] 86 | } 87 | csvr.FieldsPerRecord = fieldsPerRecord 88 | 89 | // pre-read to skip rows 90 | if skipRow > 0 { 91 | for i := 0; i < skipRow; i++ { 92 | if _, err := csvr.Read(); err != nil { 93 | return starlark.None, fmt.Errorf("%s: %w", b.Name(), err) 94 | } 95 | } 96 | } 97 | 98 | // read all rows 99 | strs, err := csvr.ReadAll() 100 | if err != nil { 101 | return starlark.None, fmt.Errorf("%s: %w", b.Name(), err) 102 | } 103 | 104 | // convert and limit rows 105 | vals := make([]starlark.Value, 0, len(strs)) 106 | for i, rowStr := range strs { 107 | if limitRow > 0 && i >= limitRow { 108 | break 109 | } 110 | row := make([]starlark.Value, len(rowStr)) 111 | for j, cell := range rowStr { 112 | row[j] = starlark.String(cell) 113 | } 114 | vals = append(vals, starlark.NewList(row)) 115 | } 116 | return starlark.NewList(vals), nil 117 | } 118 | 119 | // writeAll writes a list of lists to a csv string. 120 | func writeAll(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 121 | var ( 122 | buf = &bytes.Buffer{} 123 | data starlark.Value 124 | comma string 125 | ) 126 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "data", &data, "comma?", &comma); err != nil { 127 | return nil, err 128 | } 129 | 130 | // prepare writer 131 | csvw := csv.NewWriter(buf) 132 | if comma == "" { 133 | comma = "," 134 | } else if len(comma) != 1 { 135 | return starlark.None, fmt.Errorf("%s: expected comma param to be a single-character string", b.Name()) 136 | } 137 | csvw.Comma = []rune(comma)[0] 138 | 139 | // convert data to [][]string 140 | val, err := dataconv.Unmarshal(data) 141 | if err != nil { 142 | return starlark.None, fmt.Errorf("%s: %w", b.Name(), err) 143 | } 144 | 145 | sl, ok := val.([]interface{}) 146 | if !ok { 147 | return starlark.None, fmt.Errorf("%s: expected value to be an array type", b.Name()) 148 | } 149 | 150 | var records [][]string 151 | for i, v := range sl { 152 | sl, ok := v.([]interface{}) 153 | if !ok { 154 | return starlark.None, fmt.Errorf("%s: row %d is not an array type", b.Name(), i) 155 | } 156 | var row = make([]string, len(sl)) 157 | for j, v := range sl { 158 | row[j] = fmt.Sprintf("%v", v) 159 | } 160 | records = append(records, row) 161 | } 162 | 163 | // write all records 164 | if err := csvw.WriteAll(records); err != nil { 165 | return starlark.None, fmt.Errorf("%s: %w", b.Name(), err) 166 | } 167 | return starlark.String(buf.String()), nil 168 | } 169 | 170 | // writeDict writes a list of dictionaries to a csv string. 171 | func writeDict(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 172 | var ( 173 | buf = &bytes.Buffer{} 174 | data starlark.Value 175 | header starlark.Iterable 176 | comma string 177 | ) 178 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "data", &data, "header", &header, "comma?", &comma); err != nil { 179 | return nil, err 180 | } 181 | 182 | // prepare writer 183 | csvw := csv.NewWriter(buf) 184 | if comma == "" { 185 | comma = "," 186 | } else if len(comma) != 1 { 187 | return starlark.None, fmt.Errorf("%s: expected comma param to be a single-character string", b.Name()) 188 | } 189 | csvw.Comma = []rune(comma)[0] 190 | 191 | // convert header to []string 192 | var headerStr []string 193 | iter := header.Iterate() 194 | defer iter.Done() 195 | var hv starlark.Value 196 | for iter.Next(&hv) { 197 | s, ok := starlark.AsString(hv) 198 | if !ok { 199 | return starlark.None, fmt.Errorf("%s: for parameter header: got %s, want string", b.Name(), hv.Type()) 200 | } 201 | headerStr = append(headerStr, s) 202 | } 203 | if len(headerStr) == 0 { 204 | return starlark.None, fmt.Errorf("%s: header cannot be empty", b.Name()) 205 | } 206 | 207 | // convert data to []map[string]interface{} 208 | val, err := dataconv.Unmarshal(data) 209 | if err != nil { 210 | return starlark.None, fmt.Errorf("%s: %w", b.Name(), err) 211 | } 212 | sl, ok := val.([]interface{}) 213 | if !ok { 214 | return starlark.None, fmt.Errorf("%s: expected value to be an array type", b.Name()) 215 | } 216 | 217 | // write header 218 | var records [][]string 219 | records = append(records, headerStr) 220 | for _, m := range sl { 221 | // cast to map 222 | mm, ok := m.(map[string]interface{}) 223 | if !ok { 224 | return starlark.None, fmt.Errorf("%s: expected value to be a map type", b.Name()) 225 | } 226 | // write row 227 | var row = make([]string, len(headerStr)) 228 | for j, k := range headerStr { 229 | if v, ok := mm[k]; ok { 230 | row[j] = fmt.Sprintf("%v", v) 231 | } 232 | } 233 | records = append(records, row) 234 | } 235 | 236 | // write all records 237 | if err := csvw.WriteAll(records); err != nil { 238 | return starlark.None, fmt.Errorf("%s: %w", b.Name(), err) 239 | } 240 | return starlark.String(buf.String()), nil 241 | } 242 | -------------------------------------------------------------------------------- /lib/file/byte.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | ) 7 | 8 | var ( 9 | emptyStr string 10 | filePerm os.FileMode = 0644 11 | createFileFlag = os.O_RDWR | os.O_CREATE | os.O_TRUNC 12 | appendFileFlag = os.O_APPEND | os.O_CREATE | os.O_WRONLY 13 | ) 14 | 15 | // TrimUTF8BOM removes the leading UTF-8 byte order mark from bytes. 16 | func TrimUTF8BOM(b []byte) []byte { 17 | if len(b) >= 3 && b[0] == 0xef && b[1] == 0xbb && b[2] == 0xbf { 18 | return b[3:] 19 | } 20 | return b 21 | } 22 | 23 | // ReadFileBytes reads the whole named file and returns the contents. 24 | // It's a sugar actually, simply calls os.ReadFile like ioutil.ReadFile does since Go 1.16. 25 | func ReadFileBytes(path string) ([]byte, error) { 26 | return os.ReadFile(path) 27 | } 28 | 29 | // ReadFileString reads the whole named file and returns the contents as a string. 30 | func ReadFileString(path string) (string, error) { 31 | b, err := os.ReadFile(path) 32 | if err != nil { 33 | return emptyStr, err 34 | } 35 | return string(b), nil 36 | } 37 | 38 | // WriteFileBytes writes the given data into a file. 39 | func WriteFileBytes(path string, data []byte) error { 40 | return openFileWriteBytes(path, createFileFlag, data) 41 | } 42 | 43 | // WriteFileString writes the given content string into a file. 44 | func WriteFileString(path string, content string) error { 45 | return openFileWriteString(path, createFileFlag, content) 46 | } 47 | 48 | // AppendFileBytes writes the given data to the end of a file. 49 | func AppendFileBytes(path string, data []byte) error { 50 | return openFileWriteBytes(path, appendFileFlag, data) 51 | } 52 | 53 | // AppendFileString appends the given content string to the end of a file. 54 | func AppendFileString(path string, content string) error { 55 | return openFileWriteString(path, appendFileFlag, content) 56 | } 57 | 58 | func openFileWriteBytes(path string, flag int, data []byte) error { 59 | file, err := os.OpenFile(path, flag, filePerm) 60 | if err != nil { 61 | return err 62 | } 63 | defer file.Close() 64 | 65 | w := bufio.NewWriter(file) 66 | defer w.Flush() 67 | _, err = w.Write(data) 68 | return err 69 | } 70 | 71 | func openFileWriteString(path string, flag int, content string) error { 72 | file, err := os.OpenFile(path, flag, filePerm) 73 | if err != nil { 74 | return err 75 | } 76 | defer file.Close() 77 | 78 | w := bufio.NewWriter(file) 79 | defer w.Flush() 80 | _, err = w.WriteString(content) 81 | return err 82 | } 83 | -------------------------------------------------------------------------------- /lib/file/copy.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | 10 | "go.starlark.net/starlark" 11 | ) 12 | 13 | // copyFile is a wrapper around copyFileGo for Starlark scripts. 14 | func copyFile(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 15 | var ( 16 | src string 17 | dst string 18 | overwrite = false 19 | ) 20 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "src", &src, "dst", &dst, "overwrite?", &overwrite); err != nil { 21 | return starlark.None, err 22 | } 23 | dp, err := copyFileGo(src, dst, overwrite) 24 | if err != nil { 25 | return nil, err 26 | } 27 | return starlark.String(dp), nil 28 | } 29 | 30 | // copyFileGo copies the contents of the source file to the destination file or directory with the same mode and access and modification times. 31 | // If the destination file exists and overwrite is false, an error is returned. 32 | // Symbolic links are followed on both source and destination. 33 | // Errors occurred while setting the mode or access and modification times are ignored. 34 | func copyFileGo(src, dst string, overwrite bool) (string, error) { 35 | // No empty input 36 | if src == emptyStr { 37 | return emptyStr, errors.New("source path is empty") 38 | } 39 | if dst == emptyStr { 40 | return emptyStr, errors.New("destination path is empty") 41 | } 42 | 43 | // Open the source file. 44 | srcFile, err := os.Open(src) 45 | if err != nil { 46 | return emptyStr, fmt.Errorf("open source file: %w", err) 47 | } 48 | defer srcFile.Close() 49 | 50 | // Stat the source file to get its mode, times, and owner. 51 | srcStat, err := srcFile.Stat() 52 | if err != nil { 53 | return emptyStr, fmt.Errorf("stat source file: %w", err) 54 | } 55 | if !srcStat.Mode().IsRegular() { 56 | // HACK, not sure if this is the best way to check if the file is a regular file 57 | return emptyStr, errors.New("source file is not a regular file") 58 | } 59 | 60 | // Check if dst is a directory, and adjust the destination path if it is 61 | dstStat, err := os.Stat(dst) 62 | if err == nil { 63 | if dstStat.IsDir() { 64 | dst = filepath.Join(dst, filepath.Base(src)) 65 | // Check adjusted destination path 66 | dstStat, err = os.Stat(dst) 67 | } 68 | } 69 | if err != nil && !os.IsNotExist(err) { 70 | // for errors other than file not exists 71 | return emptyStr, err 72 | } 73 | 74 | // for destination file exists 75 | if err == nil { 76 | // If the source and destination files are the same, return an error. 77 | if os.SameFile(srcStat, dstStat) { 78 | return emptyStr, fmt.Errorf("source and destination are the same file: %s", src) 79 | } 80 | // If overwrite is false, return an error if the destination file exists. 81 | if !overwrite { 82 | return emptyStr, &os.PathError{Op: "copy", Path: dst, Err: os.ErrExist} 83 | } 84 | } 85 | 86 | // Create the destination file. 87 | dstFile, err := os.Create(dst) 88 | if err != nil { 89 | return emptyStr, fmt.Errorf("cannot create file: %w", err) 90 | } 91 | defer dstFile.Close() 92 | 93 | // Copy the source file to the destination file. 94 | if _, err := io.Copy(dstFile, srcFile); err != nil { 95 | return emptyStr, fmt.Errorf("cannot copy file: %w", err) 96 | } 97 | 98 | // Attempt to set the mode, times file to match the source file, i.e. ignore the errors 99 | _ = os.Chmod(dst, srcStat.Mode()) 100 | _ = os.Chtimes(dst, srcStat.ModTime(), srcStat.ModTime()) 101 | return dst, nil 102 | } 103 | -------------------------------------------------------------------------------- /lib/file/copy_test.go: -------------------------------------------------------------------------------- 1 | package file_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "runtime" 7 | "testing" 8 | 9 | itn "github.com/1set/starlet/internal" 10 | lf "github.com/1set/starlet/lib/file" 11 | "go.starlark.net/starlark" 12 | ) 13 | 14 | func TestLoadModule_FileCopy(t *testing.T) { 15 | isOnWindows := runtime.GOOS == "windows" 16 | tests := []struct { 17 | name string 18 | script string 19 | wantErr string 20 | skipWindows bool 21 | }{ 22 | { 23 | name: `copyfile: no args`, 24 | script: itn.HereDoc(` 25 | cf() 26 | `), 27 | wantErr: `file.copyfile: missing argument for src`, 28 | }, 29 | { 30 | name: `copyfile: src only`, 31 | script: itn.HereDoc(` 32 | cf(src=temp_file) 33 | `), 34 | wantErr: `file.copyfile: missing argument for dst`, 35 | }, 36 | { 37 | name: `copyfile: empty src`, 38 | script: itn.HereDoc(` 39 | cf(src="", dst=temp_file+"_another") 40 | `), 41 | wantErr: `source path is empty`, 42 | }, 43 | { 44 | name: `copyfile: empty dst`, 45 | script: itn.HereDoc(` 46 | cf(src=temp_file, dst="") 47 | `), 48 | wantErr: `destination path is empty`, 49 | }, 50 | { 51 | name: `copyfile: invalid args`, 52 | script: itn.HereDoc(` 53 | cf(src=temp_file, dst=temp_file+"_another", overwrite="abc") 54 | `), 55 | wantErr: `file.copyfile: for parameter "overwrite": got string, want bool`, 56 | }, 57 | { 58 | name: `normal copy`, 59 | script: itn.HereDoc(` 60 | load('file', 'stat') 61 | s, d = temp_file, temp_file+"_another" 62 | d2 = cf(s, d) 63 | assert.eq(d2, d) 64 | ss, sd = stat(s), stat(d) 65 | assert.eq(ss.type, sd.type) 66 | assert.eq(ss.size, sd.size) 67 | assert.eq(ss.modified, sd.modified) 68 | assert.eq(ss.get_md5(), sd.get_md5()) 69 | `), 70 | }, 71 | { 72 | name: `overwrite copy enabled`, 73 | script: itn.HereDoc(` 74 | load('file', 'stat') 75 | s, d = temp_file, temp_file2 76 | d2 = cf(s, d, overwrite=True) 77 | assert.eq(d2, d) 78 | ss, sd = stat(s), stat(d) 79 | assert.eq(ss.type, sd.type) 80 | assert.eq(ss.size, sd.size) 81 | assert.eq(ss.modified, sd.modified) 82 | assert.eq(ss.get_md5(), sd.get_md5()) 83 | `), 84 | wantErr: ``, 85 | }, 86 | { 87 | name: `overwrite copy disabled`, 88 | script: itn.HereDoc(` 89 | cf(temp_file, temp_file2) 90 | `), 91 | wantErr: `file already exists`, 92 | }, 93 | { 94 | name: `src dst same`, 95 | script: itn.HereDoc(` 96 | cf(temp_file, temp_file) 97 | `), 98 | wantErr: `source and destination are the same file`, 99 | }, 100 | { 101 | name: `src not exists`, 102 | script: itn.HereDoc(` 103 | s, d = temp_file + "_not", temp_file+"_another" 104 | cf(s, d) 105 | `), 106 | wantErr: `no such file or directory`, 107 | skipWindows: true, 108 | }, 109 | { 110 | name: `src is dir`, 111 | script: itn.HereDoc(` 112 | s, d = temp_dir, temp_file+"_another" 113 | cf(s, d) 114 | `), 115 | wantErr: `source file is not a regular file`, 116 | }, 117 | { 118 | name: `src is device`, 119 | script: itn.HereDoc(` 120 | s, d = "/dev/null", temp_file+"_another" 121 | cf(s, d) 122 | `), 123 | wantErr: `source file is not a regular file`, 124 | skipWindows: true, 125 | }, 126 | { 127 | name: `dst is dir`, 128 | script: itn.HereDoc(` 129 | load('file', 'stat') 130 | s, d = temp_file, temp_dir 131 | nd = cf(s, d) 132 | print("dst dir", d) 133 | print("dst file", nd) 134 | assert.ne(nd, d) 135 | ss, sd = stat(s), stat(nd) 136 | assert.eq(ss.type, sd.type) 137 | assert.eq(ss.size, sd.size) 138 | assert.eq(ss.modified, sd.modified) 139 | assert.eq(ss.get_md5(), sd.get_md5()) 140 | `), 141 | }, 142 | { 143 | name: `dst is device`, 144 | script: itn.HereDoc(` 145 | cf(temp_file, "/dev/null", overwrite=True) 146 | `), 147 | skipWindows: true, 148 | }, 149 | } 150 | for _, tt := range tests { 151 | t.Run(tt.name, func(t *testing.T) { 152 | // prepare temp file/dir if needed 153 | var ( 154 | tp string 155 | tp2 string 156 | tp3 string 157 | td string 158 | ) 159 | { 160 | // temp file 161 | if tf, err := os.CreateTemp("", "starlet-copy-test-write"); err != nil { 162 | t.Errorf("os.CreateTemp() expects no error, actual error = '%v'", err) 163 | return 164 | } else { 165 | tp = tf.Name() 166 | if err = ioutil.WriteFile(tp, []byte("Aloha"), 0644); err != nil { 167 | t.Errorf("ioutil.WriteFile() expects no error, actual error = '%v'", err) 168 | return 169 | } 170 | } 171 | // temp file 2 172 | if tf, err := os.CreateTemp("", "starlet-copy-test-write2"); err != nil { 173 | t.Errorf("os.CreateTemp() expects no error, actual error = '%v'", err) 174 | return 175 | } else { 176 | tp2 = tf.Name() 177 | if err = ioutil.WriteFile(tp2, []byte("A hui hou"), 0644); err != nil { 178 | t.Errorf("ioutil.WriteFile() expects no error, actual error = '%v'", err) 179 | return 180 | } 181 | } 182 | // temp file 3 183 | if tf, err := os.CreateTemp("", "starlet-copy-test-write3"); err != nil { 184 | t.Errorf("os.CreateTemp() expects no error, actual error = '%v'", err) 185 | return 186 | } else { 187 | tp3 = tf.Name() 188 | } 189 | // temp dir 190 | if tt, err := os.MkdirTemp("", "starlet-copy-test-dir"); err != nil { 191 | t.Errorf("os.MkdirTemp() expects no error, actual error = '%v'", err) 192 | return 193 | } else { 194 | td = tt 195 | } 196 | } 197 | 198 | // execute test 199 | if isOnWindows && tt.skipWindows { 200 | t.Skipf("Skip test on Windows") 201 | return 202 | } 203 | globals := starlark.StringDict{ 204 | "runtime_os": starlark.String(runtime.GOOS), 205 | "temp_file": starlark.String(tp), 206 | "temp_file2": starlark.String(tp2), 207 | "temp_file3": starlark.String(tp3), 208 | "temp_dir": starlark.String(td), 209 | } 210 | script := `load('file', cf='copyfile')` + "\n" + tt.script 211 | res, err := itn.ExecModuleWithErrorTest(t, lf.ModuleName, lf.LoadModule, script, tt.wantErr, globals) 212 | if (err != nil) != (tt.wantErr != "") { 213 | t.Errorf("path(%q) expects error = '%v', actual error = '%v', result = %v", tt.name, tt.wantErr, err, res) 214 | } 215 | }) 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /lib/file/json.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/1set/starlet/dataconv" 8 | "go.starlark.net/starlark" 9 | ) 10 | 11 | // readJSON reads the whole named file and decodes the contents as JSON for Starlark. 12 | func readJSON(name string) (starlark.Value, error) { 13 | data, err := ReadFileBytes(name) 14 | if err != nil { 15 | return nil, err 16 | } 17 | return dataconv.DecodeStarlarkJSON(data) 18 | } 19 | 20 | // readJSONL reads the whole named file and decodes the contents as JSON lines for Starlark. 21 | func readJSONL(name string) (starlark.Value, error) { 22 | var ( 23 | cnt int 24 | values []starlark.Value 25 | ) 26 | if err := readFileByLine(name, func(line string) error { 27 | cnt++ 28 | // skip empty lines 29 | if strings.TrimSpace(line) == emptyStr { 30 | return nil 31 | } 32 | // convert to Starlark value 33 | v, err := dataconv.DecodeStarlarkJSON([]byte(line)) 34 | if err != nil { 35 | return fmt.Errorf("line %d: %w", cnt, err) 36 | } 37 | values = append(values, v) 38 | return nil 39 | }); err != nil { 40 | return nil, err 41 | } 42 | return starlark.NewList(values), nil 43 | } 44 | 45 | // writeJSON writes the given JSON as string into a file. 46 | func writeJSON(name, funcName string, override bool, data starlark.Value) error { 47 | wf := AppendFileString 48 | if override { 49 | wf = WriteFileString 50 | } 51 | // treat starlark.Bytes and starlark.String as the same type, just convert to string, for other types, encode to JSON 52 | switch v := data.(type) { 53 | case starlark.Bytes: 54 | return wf(name, string(v)) 55 | case starlark.String: 56 | return wf(name, string(v)) 57 | default: 58 | // convert to JSON 59 | s, err := dataconv.EncodeStarlarkJSON(v) 60 | if err != nil { 61 | return err 62 | } 63 | return wf(name, s) 64 | } 65 | } 66 | 67 | // writeJSONL writes the given JSON lines into a file. 68 | func writeJSONL(name, funcName string, override bool, data starlark.Value) error { 69 | wf := AppendFileLines 70 | if override { 71 | wf = WriteFileLines 72 | } 73 | 74 | // handle all types of iterable, and allow string or bytes, for other types, encode to lines of JSON 75 | var ( 76 | ls []string 77 | err error 78 | ) 79 | switch v := data.(type) { 80 | case starlark.String: 81 | return wf(name, []string{v.GoString()}) 82 | case starlark.Bytes: 83 | return wf(name, []string{string(v)}) 84 | case *starlark.List: 85 | ls, err = convIterJSONL(v) 86 | case starlark.Tuple: 87 | ls, err = convIterJSONL(v) 88 | case *starlark.Set: 89 | ls, err = convIterJSONL(v) 90 | default: 91 | // convert to JSON 92 | s, err := dataconv.EncodeStarlarkJSON(v) 93 | if err != nil { 94 | return err 95 | } 96 | return wf(name, []string{s}) 97 | } 98 | if err != nil { 99 | return err 100 | } 101 | 102 | // write lines 103 | return wf(name, ls) 104 | } 105 | 106 | func convIterJSONL(lst starlark.Iterable) (lines []string, err error) { 107 | iter := lst.Iterate() 108 | defer iter.Done() 109 | 110 | var ( 111 | s string 112 | x starlark.Value 113 | ) 114 | for iter.Next(&x) { 115 | s, err = dataconv.EncodeStarlarkJSON(x) 116 | if err != nil { 117 | return 118 | } 119 | lines = append(lines, s) 120 | } 121 | return 122 | } 123 | -------------------------------------------------------------------------------- /lib/file/line.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "os" 9 | ) 10 | 11 | // LineFunc stands for a handler for each line string. 12 | type LineFunc func(line string) (err error) 13 | 14 | //revive:disable:error-naming It's not a real error 15 | var ( 16 | // QuitRead indicates the arbitrary error means to quit from reading. 17 | QuitRead = errors.New("file: quit read by line") 18 | ) 19 | 20 | // ReadFileLines reads all lines from the given file (the line ending chars are not included). 21 | func ReadFileLines(path string) (lines []string, err error) { 22 | err = readFileByLine(path, func(l string) error { 23 | lines = append(lines, l) 24 | return nil 25 | }) 26 | return 27 | } 28 | 29 | // CountFileLines counts all lines from the given file (the line ending chars are not included). 30 | func CountFileLines(path string) (count int, err error) { 31 | err = readFileByLine(path, func(l string) error { 32 | count++ 33 | return nil 34 | }) 35 | return 36 | } 37 | 38 | // WriteFileLines writes the given lines as a text file. 39 | func WriteFileLines(path string, lines []string) error { 40 | return openFileWriteLines(path, createFileFlag, lines) 41 | } 42 | 43 | // AppendFileLines appends the given lines to the end of a text file. 44 | func AppendFileLines(path string, lines []string) error { 45 | return openFileWriteLines(path, appendFileFlag, lines) 46 | } 47 | 48 | // ReadFirstLines reads the top n lines from the given file (the line ending chars are not included), or lesser lines if the given file doesn't contain enough line ending chars. 49 | func ReadFirstLines(path string, n int) (lines []string, err error) { 50 | var f *os.File 51 | if f, err = os.Open(path); err != nil { 52 | return 53 | } 54 | defer f.Close() 55 | return extractIOTopLines(f, n) 56 | } 57 | 58 | // ReadLastLines reads the bottom n lines from the given file (the line ending chars are not included), or lesser lines if the given file doesn't contain enough line ending chars. 59 | func ReadLastLines(path string, n int) (lines []string, err error) { 60 | var f *os.File 61 | if f, err = os.Open(path); err != nil { 62 | return 63 | } 64 | defer f.Close() 65 | return extractIOBottomLines(f, n) 66 | } 67 | 68 | // extractIOTopLines extracts the top n lines from the given stream (the line ending chars are not included), or lesser lines if the given stream doesn't contain enough line ending chars. 69 | func extractIOTopLines(rd io.Reader, n int) ([]string, error) { 70 | if n <= 0 { 71 | return nil, errors.New("n should be greater than 0") 72 | } 73 | result := make([]string, 0) 74 | if err := readIOByLine(rd, func(line string) error { 75 | result = append(result, line) 76 | n-- 77 | if n <= 0 { 78 | return QuitRead 79 | } 80 | return nil 81 | }); err != nil { 82 | return nil, err 83 | } 84 | return result, nil 85 | } 86 | 87 | // extractIOBottomLines extracts the bottom n lines from the given stream (the line ending chars are not included), or lesser lines if the given stream doesn't contain enough line ending chars. 88 | func extractIOBottomLines(rd io.Reader, n int) ([]string, error) { 89 | if n <= 0 { 90 | return nil, errors.New("n should be greater than 0") 91 | } 92 | var ( 93 | result = make([]string, n, n) 94 | cnt int 95 | ) 96 | if err := readIOByLine(rd, func(line string) error { 97 | result[cnt%n] = line 98 | cnt++ 99 | return nil 100 | }); err != nil { 101 | return nil, err 102 | } 103 | if cnt <= n { 104 | return result[0:cnt], nil 105 | } 106 | pos := cnt % n 107 | return append(result[pos:], result[0:pos]...), nil 108 | } 109 | 110 | // readFileByLine iterates the given file by lines (the line ending chars are not included). 111 | func readFileByLine(path string, callback LineFunc) (err error) { 112 | var file *os.File 113 | if file, err = os.Open(path); err != nil { 114 | return 115 | } 116 | defer file.Close() 117 | return readIOByLine(file, callback) 118 | } 119 | 120 | func openFileWriteLines(path string, flag int, lines []string) error { 121 | file, err := os.OpenFile(path, flag, filePerm) 122 | if err != nil { 123 | return err 124 | } 125 | defer file.Close() 126 | return writeIOLines(file, lines) 127 | } 128 | 129 | // writeIOLines writes the given lines to a Writer. 130 | func writeIOLines(wr io.Writer, lines []string) error { 131 | w := bufio.NewWriter(wr) 132 | defer w.Flush() 133 | for _, line := range lines { 134 | if _, err := fmt.Fprintln(w, line); err != nil { 135 | return err 136 | } 137 | } 138 | return nil 139 | } 140 | 141 | // readIOByLine iterates the given Reader by lines (the line ending chars are not included). 142 | func readIOByLine(rd io.Reader, callback LineFunc) (err error) { 143 | readLine := func(r *bufio.Reader) (string, error) { 144 | var ( 145 | err error 146 | line, ln []byte 147 | isPrefix = true 148 | ) 149 | for isPrefix && err == nil { 150 | line, isPrefix, err = r.ReadLine() 151 | ln = append(ln, line...) 152 | } 153 | return string(ln), err 154 | } 155 | r := bufio.NewReader(rd) 156 | s, e := readLine(r) 157 | for e == nil { 158 | if err = callback(s); err != nil { 159 | break 160 | } 161 | s, e = readLine(r) 162 | } 163 | 164 | if err == QuitRead { 165 | err = nil 166 | } 167 | return 168 | } 169 | -------------------------------------------------------------------------------- /lib/file/stat.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/sha1" 6 | "crypto/sha256" 7 | "crypto/sha512" 8 | "encoding/hex" 9 | "fmt" 10 | "hash" 11 | "io" 12 | "os" 13 | "path/filepath" 14 | 15 | stdtime "go.starlark.net/lib/time" 16 | "go.starlark.net/starlark" 17 | "go.starlark.net/starlarkstruct" 18 | ) 19 | 20 | // FileStat represents the file information. 21 | type FileStat struct { 22 | os.FileInfo 23 | fullPath string 24 | } 25 | 26 | // Struct returns a starlark struct with file information. 27 | func (f *FileStat) Struct() *starlarkstruct.Struct { 28 | // get file type 29 | var modeStr string 30 | switch mode := f.Mode(); { 31 | case mode.IsRegular(): 32 | modeStr = "file" 33 | case mode.IsDir(): 34 | modeStr = "dir" 35 | case mode&os.ModeSymlink != 0: 36 | modeStr = "symlink" 37 | case mode&os.ModeNamedPipe != 0: 38 | modeStr = "fifo" 39 | case mode&os.ModeSocket != 0: 40 | modeStr = "socket" 41 | case mode&os.ModeDevice != 0: 42 | if mode&os.ModeCharDevice != 0 { 43 | modeStr = "char" 44 | } else { 45 | modeStr = "block" 46 | } 47 | case mode&os.ModeIrregular != 0: 48 | modeStr = "irregular" 49 | default: 50 | modeStr = "unknown" 51 | } 52 | // create struct 53 | fileName := f.Name() 54 | fields := starlark.StringDict{ 55 | "name": starlark.String(fileName), 56 | "path": starlark.String(f.fullPath), 57 | "ext": starlark.String(filepath.Ext(fileName)), 58 | "size": starlark.MakeInt64(f.Size()), 59 | "type": starlark.String(modeStr), 60 | "modified": stdtime.Time(f.ModTime()), 61 | "get_md5": starlark.NewBuiltin("get_md5", genFileHashFunc(f.fullPath, md5.New)), 62 | "get_sha1": starlark.NewBuiltin("get_sha1", genFileHashFunc(f.fullPath, sha1.New)), 63 | "get_sha256": starlark.NewBuiltin("get_sha256", genFileHashFunc(f.fullPath, sha256.New)), 64 | "get_sha512": starlark.NewBuiltin("get_sha512", genFileHashFunc(f.fullPath, sha512.New)), 65 | } 66 | return starlarkstruct.FromStringDict(starlark.String("file_stat"), fields) 67 | } 68 | 69 | func getFileStat(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 70 | var ( 71 | inputPath string 72 | followSymlink = false 73 | ) 74 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "name", &inputPath, "follow?", &followSymlink); err != nil { 75 | return starlark.None, err 76 | } 77 | // get file stat 78 | var statFn func(string) (os.FileInfo, error) 79 | if followSymlink { 80 | statFn = os.Stat 81 | } else { 82 | statFn = os.Lstat 83 | } 84 | stat, err := statFn(inputPath) 85 | if err != nil { 86 | return none, fmt.Errorf("%s: %w", b.Name(), err) 87 | } 88 | // get file abs path 89 | absPath, err := filepath.Abs(inputPath) 90 | if err != nil { 91 | return none, fmt.Errorf("%s: %w", b.Name(), err) 92 | } 93 | // return file stat 94 | fs := &FileStat{stat, absPath} 95 | return fs.Struct(), nil 96 | } 97 | 98 | func genFileHashFunc(fp string, algo func() hash.Hash) func(*starlark.Thread, *starlark.Builtin, starlark.Tuple, []starlark.Tuple) (starlark.Value, error) { 99 | return func(t *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 100 | // open file 101 | file, err := os.Open(fp) 102 | if err != nil { 103 | return none, fmt.Errorf("%s: %w", fn.Name(), err) 104 | } 105 | defer file.Close() 106 | 107 | // get hash 108 | h := algo() 109 | if _, err := io.Copy(h, file); err != nil { 110 | return none, fmt.Errorf("%s: %w", fn.Name(), err) 111 | } 112 | return starlark.String(hex.EncodeToString(h.Sum(nil))), nil 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/file/testdata/.gitattributes: -------------------------------------------------------------------------------- 1 | *.txt binary 2 | *.json binary 3 | -------------------------------------------------------------------------------- /lib/file/testdata/1line.txt: -------------------------------------------------------------------------------- 1 | ABCDEFGHIJKLMNOPQRSTUVWXYZ甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥 -------------------------------------------------------------------------------- /lib/file/testdata/1line_nl.txt: -------------------------------------------------------------------------------- 1 | ABCDEFGHIJKLMNOPQRSTUVWXYZ甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥 2 | -------------------------------------------------------------------------------- /lib/file/testdata/aloha.txt: -------------------------------------------------------------------------------- 1 | ALOHA 2 | -------------------------------------------------------------------------------- /lib/file/testdata/bom.txt: -------------------------------------------------------------------------------- 1 | has bom -------------------------------------------------------------------------------- /lib/file/testdata/empty.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1set/starlet/bb483ca9cdfb1d68e9b39705a28a314375ee2405/lib/file/testdata/empty.txt -------------------------------------------------------------------------------- /lib/file/testdata/json1.json: -------------------------------------------------------------------------------- 1 | { 2 | "num": 42, 3 | "str": "hello", 4 | "bool": true, 5 | "arr": [1, 2, 3], 6 | "obj": { 7 | "foo": "bar", 8 | "baz": "qux" 9 | }, 10 | "undef": null 11 | } -------------------------------------------------------------------------------- /lib/file/testdata/json2.json: -------------------------------------------------------------------------------- 1 | {"name": "John", "age": 30, "city": "New York"} 2 | 3 | {"name":"Jane", "age": 25, "city": "Paris"} 4 | {"name": "Mike", "age": 32, "city": "Chicago","opt":true} 5 | -------------------------------------------------------------------------------- /lib/file/testdata/line_mac.txt: -------------------------------------------------------------------------------- 1 | Line 1 2 | Line 2 3 | Line 3 4 | -------------------------------------------------------------------------------- /lib/file/testdata/line_win.txt: -------------------------------------------------------------------------------- 1 | Line 1 2 | Line 2 3 | Line 3 4 | -------------------------------------------------------------------------------- /lib/file/testdata/noext: -------------------------------------------------------------------------------- 1 | no 2 | -------------------------------------------------------------------------------- /lib/hashlib/README.md: -------------------------------------------------------------------------------- 1 | # hashlib 2 | 3 | `hashlib` defines hash primitives for Starlark. 4 | 5 | ## Functions 6 | 7 | ### `md5(data) string` 8 | 9 | Returns an MD5 hash for a string or bytes. 10 | 11 | #### Examples 12 | 13 | **Basic** 14 | 15 | Calculate an MD5 checksum for "hello world". 16 | 17 | ```python 18 | load("hashlib", "md5") 19 | sum = md5("hello world!") 20 | print(sum) 21 | # Output: fc3ff98e8c6a0d3087d515c0473f8677 22 | ``` 23 | 24 | ### `sha1(data) string` 25 | 26 | Returns a SHA-1 hash for a string or bytes. 27 | 28 | #### Examples 29 | 30 | **Basic** 31 | 32 | Calculate an SHA-1 checksum for "hello world". 33 | 34 | ```python 35 | load("hashlib", "sha1") 36 | sum = sha1("hello world!") 37 | print(sum) 38 | # Output: 430ce34d020724ed75a196dfc2ad67c77772d169 39 | ``` 40 | 41 | ### `sha256(data) string` 42 | 43 | Returns an SHA-256 hash for a string or bytes. 44 | 45 | #### Examples 46 | 47 | **Basic** 48 | 49 | Calculate an SHA-256 checksum for "hello world". 50 | 51 | ```python 52 | load("hashlib", "sha256") 53 | sum = sha256("hello world!") 54 | print(sum) 55 | # Output: 7509e5bda0c762d2bac7f90d758b5b2263fa01ccbc542ab5e3df163be08e6ca9 56 | ``` 57 | 58 | ### `sha512(data) string` 59 | 60 | Returns an SHA-512 hash for a string or bytes. 61 | 62 | #### Examples 63 | 64 | **Basic** 65 | 66 | Calculate an SHA-512 checksum for "hello world". 67 | 68 | ```python 69 | load("hashlib", "sha512") 70 | sum = sha512("hello world!") 71 | print(sum) 72 | # Output: db9b1cd3262dee37756a09b9064973589847caa8e53d31a9d142ea2701b1b28abd97838bb9a27068ba305dc8d04a45a1fcf079de54d607666996b3cc54f6b67c 73 | ``` 74 | -------------------------------------------------------------------------------- /lib/hashlib/hash.go: -------------------------------------------------------------------------------- 1 | // Package hashlib defines hash primitives for Starlark. 2 | // 3 | // Migrated from: https://github.com/qri-io/starlib/tree/master/hash 4 | package hashlib 5 | 6 | import ( 7 | "crypto/md5" 8 | "crypto/sha1" 9 | "crypto/sha256" 10 | "crypto/sha512" 11 | "encoding/hex" 12 | "hash" 13 | "io" 14 | "sync" 15 | 16 | tps "github.com/1set/starlet/dataconv/types" 17 | "go.starlark.net/starlark" 18 | "go.starlark.net/starlarkstruct" 19 | ) 20 | 21 | // ModuleName defines the expected name for this Module when used 22 | // in starlark's load() function, eg: load('hashlib', 'md5') 23 | const ModuleName = "hashlib" 24 | 25 | var ( 26 | once sync.Once 27 | hashModule starlark.StringDict 28 | hashError error 29 | ) 30 | 31 | // LoadModule loads the hashlib module. It is concurrency-safe and idempotent. 32 | func LoadModule() (starlark.StringDict, error) { 33 | once.Do(func() { 34 | hashModule = starlark.StringDict{ 35 | "hashlib": &starlarkstruct.Module{ 36 | Name: "hashlib", 37 | Members: starlark.StringDict{ 38 | "md5": starlark.NewBuiltin("hash.md5", fnHash(md5.New)), 39 | "sha1": starlark.NewBuiltin("hash.sha1", fnHash(sha1.New)), 40 | "sha256": starlark.NewBuiltin("hash.sha256", fnHash(sha256.New)), 41 | "sha512": starlark.NewBuiltin("hash.sha512", fnHash(sha512.New)), 42 | }, 43 | }, 44 | } 45 | }) 46 | return hashModule, hashError 47 | } 48 | 49 | func fnHash(algo func() hash.Hash) func(*starlark.Thread, *starlark.Builtin, starlark.Tuple, []starlark.Tuple) (starlark.Value, error) { 50 | return func(t *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 51 | // check args 52 | var sb tps.StringOrBytes 53 | if err := starlark.UnpackArgs(fn.Name(), args, kwargs, "data", &sb); err != nil { 54 | return starlark.None, err 55 | } 56 | 57 | // get hash 58 | h := algo() 59 | _, err := io.WriteString(h, sb.GoString()) 60 | if err != nil { 61 | return starlark.None, err 62 | } 63 | return starlark.String(hex.EncodeToString(h.Sum(nil))), nil 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/hashlib/hash_test.go: -------------------------------------------------------------------------------- 1 | package hashlib_test 2 | 3 | import ( 4 | "testing" 5 | 6 | itn "github.com/1set/starlet/internal" 7 | "github.com/1set/starlet/lib/hashlib" 8 | ) 9 | 10 | func TestLoadModule_Hash(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | script string 14 | wantErr string 15 | }{ 16 | { 17 | name: `MD5`, 18 | script: itn.HereDoc(` 19 | load('hashlib', 'md5') 20 | assert.eq(md5(""), "d41d8cd98f00b204e9800998ecf8427e") 21 | assert.eq(md5("Aloha!"), "de424bf3e7dcba091c27d652ada485fb") 22 | assert.eq(md5(b"Aloha!"), "de424bf3e7dcba091c27d652ada485fb") 23 | `), 24 | }, 25 | { 26 | name: `SHA1`, 27 | script: itn.HereDoc(` 28 | load('hashlib', 'sha1') 29 | assert.eq(sha1(""), "da39a3ee5e6b4b0d3255bfef95601890afd80709") 30 | assert.eq(sha1("Aloha!"), "c3dd37312ba987e1cc40ae021bc202c4a52d8afe") 31 | `), 32 | }, 33 | { 34 | name: `SHA256`, 35 | script: itn.HereDoc(` 36 | load('hashlib', 'sha256') 37 | assert.eq(sha256(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") 38 | assert.eq(sha256("Aloha!"), "dea7e28aee505f2dd033de1427a517793e38b7605e8fc24da40151907e52cea3") 39 | `), 40 | }, 41 | { 42 | name: `SHA512`, 43 | script: itn.HereDoc(` 44 | load('hashlib', 'sha512') 45 | assert.eq(sha512(""), "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e") 46 | assert.eq(sha512("Aloha!"), "d9cb95ad9d916a0781b3339424d5eb11c476405dfba7af7fabf4981fdd3291c27e8006e4cca617beae70dd00ab86a0213c44ed461229b16b45db45f64691049e") 47 | `), 48 | }, 49 | { 50 | name: `Invalid Argument Count`, 51 | script: itn.HereDoc(` 52 | load('hashlib', 'md5') 53 | md5("Aloha!", "Hello!") 54 | `), 55 | wantErr: "hash.md5: got 2 arguments, want at most 1", 56 | }, 57 | { 58 | name: `Invalid Input Type`, 59 | script: itn.HereDoc(` 60 | load('hashlib', 'md5') 61 | md5(123) 62 | assert.fail("should not reach here") 63 | `), 64 | wantErr: "hash.md5: for parameter data: got int, want string or bytes", 65 | }, 66 | } 67 | for _, tt := range tests { 68 | t.Run(tt.name, func(t *testing.T) { 69 | res, err := itn.ExecModuleWithErrorTest(t, hashlib.ModuleName, hashlib.LoadModule, tt.script, tt.wantErr, nil) 70 | if (err != nil) != (tt.wantErr != "") { 71 | t.Errorf("hash(%q) expects error = '%v', actual error = '%v', result = %v", tt.name, tt.wantErr, err, res) 72 | return 73 | } 74 | }) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/http/internal_test.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/1set/starlet/dataconv/types" 10 | "go.starlark.net/starlark" 11 | ) 12 | 13 | // we're ok with testing private functions if it simplifies the test :) 14 | func TestSetBody(t *testing.T) { 15 | fd := map[string]string{ 16 | "foo": "bar baz", 17 | } 18 | 19 | cases := []struct { 20 | rawBody *types.NullableStringOrBytes 21 | formData map[string]string 22 | formEncoding starlark.String 23 | jsonData starlark.Value 24 | body string 25 | err string 26 | }{ 27 | {types.NewNullableStringOrBytes("hallo"), nil, starlark.String(""), nil, "hallo", ""}, 28 | {types.NewNullableStringOrBytes(""), fd, starlark.String(""), nil, "foo=bar+baz", ""}, 29 | // TODO - this should check multipart form data is being set 30 | {nil, fd, starlark.String("multipart/form-data"), nil, "", ""}, 31 | {nil, nil, starlark.String(""), starlark.Tuple{starlark.Bool(true), starlark.MakeInt(1), starlark.String("der")}, "[true,1,\"der\"]", ""}, 32 | } 33 | 34 | for i, c := range cases { 35 | var formData *starlark.Dict 36 | if c.formData != nil { 37 | formData = starlark.NewDict(len(c.formData)) 38 | for k, v := range c.formData { 39 | if err := formData.SetKey(starlark.String(k), starlark.String(v)); err != nil { 40 | t.Fatal(err) 41 | } 42 | } 43 | } 44 | 45 | req := httptest.NewRequest("get", "https://example.com", nil) 46 | err := setBody(req, c.rawBody, formData, c.formEncoding, c.jsonData) 47 | if !(err == nil && c.err == "" || (err != nil && err.Error() == c.err)) { 48 | t.Errorf("case %d error mismatch. expected: %s, got: %s", i, c.err, err) 49 | continue 50 | } 51 | 52 | if strings.HasPrefix(req.Header.Get("Content-Type"), "multipart/form-data;") { 53 | if err := req.ParseMultipartForm(0); err != nil { 54 | t.Fatal(err) 55 | } 56 | 57 | for k, v := range c.formData { 58 | fv := req.FormValue(k) 59 | if fv != v { 60 | t.Errorf("case %d error mismatch. expected %s=%s, got: %s", i, k, v, fv) 61 | } 62 | } 63 | } else { 64 | body, err := ioutil.ReadAll(req.Body) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | if string(body) != c.body { 70 | t.Errorf("case %d body mismatch. expected: %s, got: %s", i, c.body, string(body)) 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lib/log/README.md: -------------------------------------------------------------------------------- 1 | # log 2 | 3 | `log` provides functionality for logging messages at various severity levels. 4 | 5 | ## Functions 6 | 7 | ### `debug(msg, *misc, **kv)` 8 | 9 | Logs a message at the debug log level. 10 | 11 | #### Parameters 12 | 13 | | name | type | description | 14 | |--------|------------|-----------------------------------------------------------------------------------------------| 15 | | `msg` | `string` | The message to log. | 16 | | `misc` | `*args` | Additional message arguments will be concatenated to the message string separated by a space. | 17 | | `kv` | `**kwargs` | Key-value pairs to provide additional debug information. | 18 | 19 | #### Examples 20 | 21 | **basic** 22 | 23 | Log a debug message with additional information. 24 | 25 | ```python 26 | load("log", "debug") 27 | debug("Fetching data at", "21:40", retry_attempt=1) 28 | {"retry_attempt": 1} 29 | ``` 30 | 31 | ### `info(msg, *misc, **kv)` 32 | 33 | Logs a message at the info log level. 34 | 35 | #### Parameters 36 | 37 | | name | type | description | 38 | |--------|------------|-----------------------------------------------------------------------------------------------| 39 | | `msg` | `string` | The message to log. | 40 | | `misc` | `*args` | Additional message arguments will be concatenated to the message string separated by a space. | 41 | | `kv` | `**kwargs` | Key-value pairs to provide additional information. | 42 | 43 | #### Examples 44 | 45 | **basic** 46 | 47 | Log an info message with additional information. 48 | 49 | ```python 50 | load("log", "info") 51 | info("Data fetched", response_time=42) 52 | ``` 53 | 54 | ### `warn(msg, *misc, **kv)` 55 | 56 | Logs a message at the warn log level. 57 | 58 | #### Parameters 59 | 60 | | name | type | description | 61 | |--------|------------|-----------------------------------------------------------------------------------------------| 62 | | `msg` | `string` | The message to log. | 63 | | `misc` | `*args` | Additional message arguments will be concatenated to the message string separated by a space. | 64 | | `kv` | `**kwargs` | Key-value pairs to provide additional warning information. | 65 | 66 | #### Examples 67 | 68 | **basic** 69 | 70 | Log a warning message with additional information. 71 | 72 | ```python 73 | load("log", "warn") 74 | warn("Fetching data took longer than expected", response_time=123) 75 | ``` 76 | 77 | ### `error(msg, *misc, **kv)` 78 | 79 | Logs a message at the error log level and returns an error. 80 | 81 | #### Parameters 82 | 83 | | name | type | description | 84 | |--------|------------|-----------------------------------------------------------------------------------------------| 85 | | `msg` | `string` | The message to log. | 86 | | `misc` | `*args` | Additional message arguments will be concatenated to the message string separated by a space. | 87 | | `kv` | `**kwargs` | Key-value pairs to provide additional error information. | 88 | 89 | #### Examples 90 | 91 | **basic** 92 | 93 | Log an error message with additional information. 94 | 95 | ```python 96 | load("log", "error") 97 | error("Failed to fetch data", response_time=240) 98 | ``` 99 | 100 | ### `fatal(msg, *misc, **kv)` 101 | 102 | Logs a message at the error log level, returns a `fail(msg)` to halt program execution. 103 | 104 | #### Parameters 105 | 106 | | name | type | description | 107 | |--------|------------|-----------------------------------------------------------------------------------------------| 108 | | `msg` | `string` | The message to log. | 109 | | `misc` | `*args` | Additional message arguments will be concatenated to the message string separated by a space. | 110 | | `kv` | `**kwargs` | Key-value pairs to provide additional fatal error information. | 111 | 112 | #### Examples 113 | 114 | **basic** 115 | 116 | Log a fatal error message with additional information. 117 | 118 | ```python 119 | load("log", "fatal") 120 | fatal("Failed to fetch data and cannot recover", retry_attempts=3, response_time=360) 121 | ``` 122 | -------------------------------------------------------------------------------- /lib/log/zaplog.go: -------------------------------------------------------------------------------- 1 | // Package log provides functionality for logging messages at various severity levels in the Starlark environment. 2 | package log 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "strings" 8 | "sync" 9 | 10 | dc "github.com/1set/starlet/dataconv" 11 | "go.starlark.net/starlark" 12 | "go.starlark.net/starlarkstruct" 13 | "go.uber.org/zap" 14 | "go.uber.org/zap/zapcore" 15 | ) 16 | 17 | // ModuleName defines the expected name for this Module when used 18 | // in starlark's load() function, eg: load('log', 'info') 19 | const ModuleName = "log" 20 | 21 | // Initialized as global functions to be used as default 22 | var ( 23 | defaultModule = NewModule(NewDefaultLogger()) 24 | // LoadModule loads the default log module. It is concurrency-safe and idempotent. 25 | LoadModule = defaultModule.LoadModule 26 | // SetLog sets the logger of the default log module from outside the package. If l is nil, a noop logger is used, which does nothing. 27 | SetLog = defaultModule.SetLog 28 | ) 29 | 30 | // NewDefaultLogger creates a new logger as a default. It is used when no logger is provided to NewModule. 31 | func NewDefaultLogger() *zap.SugaredLogger { 32 | cfg := zap.NewDevelopmentConfig() 33 | cfg.DisableCaller = true 34 | cfg.DisableStacktrace = true 35 | lg, _ := cfg.Build() 36 | return lg.Sugar() 37 | } 38 | 39 | // Module wraps the starlark module for the log package. 40 | type Module struct { 41 | once sync.Once 42 | logModule starlark.StringDict 43 | logger *zap.SugaredLogger 44 | } 45 | 46 | // NewModule creates a new log module. If logger is nil, a new development logger is created. 47 | func NewModule(lg *zap.SugaredLogger) *Module { 48 | if lg == nil { 49 | lg = NewDefaultLogger() 50 | } 51 | return &Module{logger: lg} 52 | } 53 | 54 | // LoadModule returns the log module loader. It is concurrency-safe and idempotent. 55 | func (m *Module) LoadModule() (starlark.StringDict, error) { 56 | m.once.Do(func() { 57 | // If logger is nil, create a new development logger. 58 | if m.logger == nil { 59 | m.logger = NewDefaultLogger() 60 | } 61 | 62 | // Create the log module 63 | m.logModule = starlark.StringDict{ 64 | ModuleName: &starlarkstruct.Module{ 65 | Name: ModuleName, 66 | Members: starlark.StringDict{ 67 | "debug": m.genLoggerBuiltin("debug", zap.DebugLevel), 68 | "info": m.genLoggerBuiltin("info", zap.InfoLevel), 69 | "warn": m.genLoggerBuiltin("warn", zap.WarnLevel), 70 | "error": m.genLoggerBuiltin("error", zap.ErrorLevel), 71 | "fatal": m.genLoggerBuiltin("fatal", zap.FatalLevel), 72 | }, 73 | }, 74 | } 75 | }) 76 | return m.logModule, nil 77 | } 78 | 79 | // SetLog sets the logger of the log module from outside the package. If l is nil, a noop logger is used, which does nothing. 80 | func (m *Module) SetLog(l *zap.SugaredLogger) { 81 | if l == nil { 82 | m.logger = zap.NewNop().Sugar() 83 | return 84 | } 85 | m.logger = l 86 | } 87 | 88 | // genLoggerBuiltin is a helper function to generate a starlark Builtin function that logs a message at a given level. 89 | func (m *Module) genLoggerBuiltin(name string, level zapcore.Level) starlark.Callable { 90 | return starlark.NewBuiltin(ModuleName+"."+name, func(thread *starlark.Thread, fn *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 91 | var msg string 92 | if len(args) <= 0 { 93 | return nil, fmt.Errorf("%s: expected at least 1 argument, got 0", fn.Name()) 94 | } else if s, ok := args[0].(starlark.String); ok { 95 | msg = string(s) 96 | } else { 97 | return nil, fmt.Errorf("%s: expected string as first argument, got %s", fn.Name(), args[0].Type()) 98 | } 99 | 100 | // find the correct log function 101 | var ( 102 | logFn func(msg string, keysAndValues ...interface{}) 103 | retErr bool 104 | ) 105 | switch level { 106 | case zap.DebugLevel: 107 | logFn = m.logger.Debugw 108 | case zap.InfoLevel: 109 | logFn = m.logger.Infow 110 | case zap.WarnLevel: 111 | logFn = m.logger.Warnw 112 | case zap.ErrorLevel: 113 | logFn = m.logger.Errorw 114 | case zap.FatalLevel: 115 | logFn = m.logger.Errorw 116 | retErr = true 117 | default: 118 | return nil, fmt.Errorf("unsupported log level: %v", level) 119 | } 120 | 121 | // append leftover arguments to message 122 | if len(args) > 1 { 123 | var ps []string 124 | for _, a := range args[1:] { 125 | ps = append(ps, dc.StarString(a)) 126 | } 127 | msg += " " + strings.Join(ps, " ") 128 | } 129 | 130 | // convert args to key-value pairs 131 | var kvp []interface{} 132 | for _, pair := range kwargs { 133 | // for each key-value pair 134 | if pair.Len() != 2 { 135 | continue 136 | } 137 | key, val := pair[0], pair[1] 138 | 139 | // for keys, try to interpret as string, or use String() as fallback 140 | kvp = append(kvp, dc.StarString(key)) 141 | 142 | // for values, try to unmarshal to Go types, or use String() as fallback 143 | if v, e := dc.Unmarshal(val); e == nil { 144 | kvp = append(kvp, v) 145 | } else { 146 | kvp = append(kvp, val.String()) 147 | } 148 | } 149 | 150 | // log the message 151 | logFn(msg, kvp...) 152 | if retErr { 153 | return starlark.None, errors.New(msg) 154 | } 155 | return starlark.None, nil 156 | }) 157 | } 158 | -------------------------------------------------------------------------------- /lib/log/zaplog_test.go: -------------------------------------------------------------------------------- 1 | package log_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | itn "github.com/1set/starlet/internal" 10 | lg "github.com/1set/starlet/lib/log" 11 | "go.uber.org/zap" 12 | "go.uber.org/zap/zapcore" 13 | ) 14 | 15 | func TestLoadModule_Log_NoSet(t *testing.T) { 16 | mm := lg.NewModule(nil) 17 | script := "load('log', 'info')\ninfo('this is 1st info message only')" 18 | res, err := itn.ExecModuleWithErrorTest(t, lg.ModuleName, mm.LoadModule, script, "", nil) 19 | if err != nil { 20 | t.Errorf("log.SetLog(nil) expects no error, actual error = '%v', result = %v", err, res) 21 | return 22 | } 23 | } 24 | 25 | func TestLoadModule_Log_SetLog_Nil(t *testing.T) { 26 | mm := lg.NewModule(nil) 27 | mm.SetLog(nil) 28 | script := "load('log', 'info')\ninfo('this is 2nd info message only')" 29 | res, err := itn.ExecModuleWithErrorTest(t, lg.ModuleName, mm.LoadModule, script, "", nil) 30 | if err != nil { 31 | t.Errorf("log.SetLog(nil) expects no error, actual error = '%v', result = %v", err, res) 32 | return 33 | } 34 | } 35 | 36 | func TestLoadModule_Log(t *testing.T) { 37 | tests := []struct { 38 | name string 39 | script string 40 | wantErr string 41 | keywords []string 42 | }{ 43 | { 44 | name: `debug message`, 45 | script: itn.HereDoc(` 46 | load('log', 'debug') 47 | debug('this is a debug message only') 48 | `), 49 | keywords: []string{"DEBUG", "this is a debug message only"}, 50 | }, 51 | { 52 | name: `debug with no args`, 53 | script: itn.HereDoc(` 54 | load('log', 'debug') 55 | debug() 56 | `), 57 | wantErr: "log.debug: expected at least 1 argument, got 0", 58 | }, 59 | { 60 | name: `debug with invalid arg type`, 61 | script: itn.HereDoc(` 62 | load('log', 'debug') 63 | debug(520) 64 | `), 65 | wantErr: "log.debug: expected string as first argument, got int", 66 | }, 67 | { 68 | name: `debug with pending args`, 69 | script: itn.HereDoc(` 70 | load('log', 'debug') 71 | debug('this is a broken message', "what", 123, True) 72 | `), 73 | keywords: []string{"DEBUG", `this is a broken message what 123 True`}, 74 | }, 75 | { 76 | name: `debug with key values`, 77 | script: itn.HereDoc(` 78 | load('log', 'debug') 79 | m = {"mm": "this is more"} 80 | l = [2, "LIST", 3.14, True] 81 | debug('this is a data message', map=m, list=l) 82 | `), 83 | keywords: []string{"DEBUG", "this is a data message", `{"map": {"mm":"this is more"}, "list": [2,"LIST",3.14,true]}`}, 84 | }, 85 | { 86 | name: `info message`, 87 | script: itn.HereDoc(` 88 | load('log', 'info') 89 | info('this is an info message', a1=2, hello="world") 90 | `), 91 | keywords: []string{"INFO", "this is an info message", `{"a1": 2, "hello": "world"}`}, 92 | }, 93 | { 94 | name: `info self args`, 95 | script: itn.HereDoc(` 96 | load('log', 'info') 97 | d = {"hello": "world"} 98 | d["a"] = d 99 | l = [1,2,3] 100 | l.append(l) 101 | s = set([4,5,6]) 102 | info('this is complex info message', self1=d, self2=l, self3=s) 103 | `), 104 | keywords: []string{"INFO", "this is complex info message", `{"self1": "{\"hello\": \"world\", \"a\": {...}}", "self2": "[1, 2, 3, [...]]", "self3": [4,5,6]}`}, 105 | }, 106 | { 107 | name: `warn message`, 108 | script: itn.HereDoc(` 109 | load('log', 'warn') 110 | warn('this is a warning message only') 111 | `), 112 | keywords: []string{"WARN", "this is a warning message only"}, 113 | }, 114 | { 115 | name: `error message`, 116 | script: itn.HereDoc(` 117 | load('log', 'error') 118 | error('this is an error message only', dsat=None) 119 | `), 120 | keywords: []string{"ERROR", "this is an error message only", `{"dsat": null}`}, 121 | }, 122 | { 123 | name: `fatal message`, 124 | script: itn.HereDoc(` 125 | load('log', 'fatal') 126 | fatal('this is a fatal message only') 127 | `), 128 | wantErr: `this is a fatal message only`, 129 | }, 130 | } 131 | for _, tt := range tests { 132 | t.Run(tt.name, func(t *testing.T) { 133 | l, b := buildCustomLogger() 134 | lg.SetLog(l) 135 | res, err := itn.ExecModuleWithErrorTest(t, lg.ModuleName, lg.LoadModule, tt.script, tt.wantErr, nil) 136 | if (err != nil) != (tt.wantErr != "") { 137 | t.Errorf("log(%q) expects error = '%v', actual error = '%v', result = %v", tt.name, tt.wantErr, err, res) 138 | return 139 | } 140 | if len(tt.wantErr) > 0 { 141 | return 142 | } 143 | if len(tt.keywords) > 0 { 144 | bs := b.String() 145 | for _, k := range tt.keywords { 146 | if !strings.Contains(bs, k) { 147 | t.Errorf("log(%q) expects keyword = '%v', actual log = '%v'", tt.name, k, bs) 148 | return 149 | } 150 | } 151 | } else { 152 | fmt.Println(b.String()) 153 | } 154 | }) 155 | } 156 | } 157 | 158 | func buildCustomLogger() (*zap.SugaredLogger, *bytes.Buffer) { 159 | buf := bytes.NewBufferString("") 160 | var al zap.LevelEnablerFunc = func(lvl zapcore.Level) bool { 161 | return true 162 | } 163 | ce := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ 164 | TimeKey: "time", 165 | LevelKey: "level", 166 | NameKey: "logger", 167 | CallerKey: "caller", 168 | MessageKey: "msg", 169 | StacktraceKey: "stacktrace", 170 | LineEnding: zapcore.DefaultLineEnding, 171 | EncodeLevel: zapcore.CapitalLevelEncoder, 172 | EncodeTime: zapcore.ISO8601TimeEncoder, 173 | EncodeDuration: zapcore.StringDurationEncoder, 174 | EncodeCaller: zapcore.ShortCallerEncoder, 175 | }) 176 | cr := zapcore.NewCore(ce, zapcore.AddSync(buf), al) 177 | op := []zap.Option{ 178 | zap.AddCaller(), 179 | //zap.AddStacktrace(zap.ErrorLevel), 180 | } 181 | logger := zap.New(cr, op...) 182 | return logger.Sugar(), buf 183 | } 184 | -------------------------------------------------------------------------------- /lib/net/network.go: -------------------------------------------------------------------------------- 1 | // Package net provides network-related functions for Starlark, inspired by Go's net package and Python's socket module. 2 | package net 3 | 4 | import ( 5 | "context" 6 | "fmt" 7 | "net" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | "github.com/1set/starlet/dataconv" 13 | tps "github.com/1set/starlet/dataconv/types" 14 | "go.starlark.net/starlark" 15 | "go.starlark.net/starlarkstruct" 16 | ) 17 | 18 | // ModuleName defines the expected name for this Module when used in starlark's load() function, eg: load('net', 'tcping') 19 | const ModuleName = "net" 20 | 21 | var ( 22 | none = starlark.None 23 | once sync.Once 24 | modFunc starlark.StringDict 25 | ) 26 | 27 | // LoadModule loads the net module. It is concurrency-safe and idempotent. 28 | func LoadModule() (starlark.StringDict, error) { 29 | once.Do(func() { 30 | modFunc = starlark.StringDict{ 31 | ModuleName: &starlarkstruct.Module{ 32 | Name: ModuleName, 33 | Members: starlark.StringDict{ 34 | "nslookup": starlark.NewBuiltin(ModuleName+".nslookup", starLookup), 35 | "tcping": starlark.NewBuiltin(ModuleName+".tcping", starTCPPing), 36 | "httping": starlark.NewBuiltin(ModuleName+".httping", starHTTPing), 37 | }, 38 | }, 39 | } 40 | }) 41 | return modFunc, nil 42 | } 43 | 44 | func goLookup(ctx context.Context, domain, dnsServer string, timeout time.Duration) ([]string, error) { 45 | // create a custom resolver if a DNS server is specified 46 | var r *net.Resolver 47 | if dnsServer != "" { 48 | if !strings.Contains(dnsServer, ":") { 49 | // append default DNS port if not specified 50 | dnsServer = net.JoinHostPort(dnsServer, "53") 51 | } 52 | r = &net.Resolver{ 53 | PreferGo: true, 54 | Dial: func(ctx context.Context, network, address string) (net.Conn, error) { 55 | d := net.Dialer{ 56 | Timeout: timeout, 57 | } 58 | return d.DialContext(ctx, "udp", dnsServer) 59 | }, 60 | } 61 | } else { 62 | r = net.DefaultResolver 63 | } 64 | 65 | // Create a new context with timeout 66 | ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout) 67 | defer cancel() 68 | 69 | // perform the DNS lookup 70 | return r.LookupHost(ctxWithTimeout, domain) 71 | } 72 | 73 | func starLookup(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 74 | var ( 75 | domain tps.StringOrBytes 76 | dnsServer tps.NullableStringOrBytes 77 | timeout tps.FloatOrInt = 10 78 | ) 79 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "domain", &domain, "dns_server?", &dnsServer, "timeout?", &timeout); err != nil { 80 | return nil, err 81 | } 82 | 83 | // correct timeout value 84 | if timeout <= 0 { 85 | timeout = 10 86 | } 87 | 88 | // get the context 89 | ctx := dataconv.GetThreadContext(thread) 90 | 91 | // perform the DNS lookup 92 | ips, err := goLookup(ctx, domain.GoString(), dnsServer.GoString(), time.Duration(timeout)*time.Second) 93 | 94 | // return the result 95 | if err != nil { 96 | return none, fmt.Errorf("%s: %w", b.Name(), err) 97 | } 98 | var list []starlark.Value 99 | for _, ip := range ips { 100 | list = append(list, starlark.String(ip)) 101 | } 102 | return starlark.NewList(list), nil 103 | } 104 | -------------------------------------------------------------------------------- /lib/net/network_test.go: -------------------------------------------------------------------------------- 1 | package net_test 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | itn "github.com/1set/starlet/internal" 8 | "github.com/1set/starlet/lib/net" 9 | ) 10 | 11 | func TestLoadModule_NSLookUp(t *testing.T) { 12 | isOnWindows := runtime.GOOS == "windows" 13 | tests := []struct { 14 | name string 15 | script string 16 | wantErr string 17 | skipWindows bool 18 | }{ 19 | { 20 | name: `nslookup: normal`, 21 | script: itn.HereDoc(` 22 | load('net', 'nslookup') 23 | ips = nslookup('bing.com') 24 | print(ips) 25 | assert.true(len(ips) > 0) 26 | `), 27 | }, 28 | { 29 | name: `nslookup: normal with timeout`, 30 | script: itn.HereDoc(` 31 | load('net', 'nslookup') 32 | ips = nslookup('bing.com', timeout=5) 33 | print(ips) 34 | assert.true(len(ips) > 0) 35 | `), 36 | }, 37 | { 38 | name: `nslookup: normal with dns`, 39 | script: itn.HereDoc(` 40 | load('net', 'nslookup') 41 | ips = nslookup('bing.com', '8.8.8.8') 42 | print(ips) 43 | assert.true(len(ips) > 0) 44 | `), 45 | }, 46 | { 47 | name: `nslookup: normal with dns:port`, 48 | script: itn.HereDoc(` 49 | load('net', 'nslookup') 50 | ips = nslookup('bing.com', '1.1.1.1:53') 51 | print(ips) 52 | assert.true(len(ips) > 0) 53 | `), 54 | }, 55 | { 56 | name: `nslookup: ip`, 57 | script: itn.HereDoc(` 58 | load('net', 'nslookup') 59 | ips = nslookup('8.8.8.8', timeout=-1) 60 | print(ips) 61 | assert.true(len(ips) > 0) 62 | `), 63 | }, 64 | { 65 | name: `nslookup: localhost`, 66 | script: itn.HereDoc(` 67 | load('net', 'nslookup') 68 | ips = nslookup('localhost') 69 | print(ips) 70 | assert.true(len(ips) > 0) 71 | `), 72 | }, 73 | { 74 | name: `nslookup: not exists`, 75 | script: itn.HereDoc(` 76 | load('net', 'nslookup') 77 | ips = nslookup('missing.invalid') 78 | `), 79 | wantErr: `missing.invalid`, // mac/win: no such host, linux: server misbehaving 80 | }, 81 | { 82 | name: `nslookup: wrong dns`, 83 | script: itn.HereDoc(` 84 | load('net', 'nslookup') 85 | ips = nslookup('bing.com', 'microsoft.com', timeout=1) 86 | `), 87 | wantErr: `timeout`, // Accept any error containing "timeout" 88 | skipWindows: true, // on Windows 2022 with Go 1.18.10, it returns results from the default DNS server 89 | }, 90 | { 91 | name: `nslookup: no args`, 92 | script: itn.HereDoc(` 93 | load('net', 'nslookup') 94 | nslookup() 95 | `), 96 | wantErr: `net.nslookup: missing argument for domain`, 97 | }, 98 | { 99 | name: `nslookup: invalid args`, 100 | script: itn.HereDoc(` 101 | load('net', 'nslookup') 102 | nslookup(1, 2, 3) 103 | `), 104 | wantErr: `net.nslookup: for parameter domain: got int, want string or bytes`, 105 | }, 106 | } 107 | for _, tt := range tests { 108 | t.Run(tt.name, func(t *testing.T) { 109 | if isOnWindows && tt.skipWindows { 110 | t.Skipf("Skip test on Windows") 111 | return 112 | } 113 | res, err := itn.ExecModuleWithErrorTest(t, net.ModuleName, net.LoadModule, tt.script, tt.wantErr, nil) 114 | if (err != nil) != (tt.wantErr != "") { 115 | t.Errorf("net(%q) expects error = '%v', actual error = '%v', result = %v", tt.name, tt.wantErr, err, res) 116 | return 117 | } 118 | }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /lib/net/ping.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "net/http/httptrace" 9 | "strconv" 10 | "sync" 11 | "time" 12 | 13 | "github.com/1set/starlet/dataconv" 14 | tps "github.com/1set/starlet/dataconv/types" 15 | "github.com/montanaflynn/stats" 16 | "go.starlark.net/starlark" 17 | "go.starlark.net/starlarkstruct" 18 | ) 19 | 20 | func goPingWrap(ctx context.Context, address string, count int, timeout, interval time.Duration, pingFunc func(ctx context.Context, address string, timeout time.Duration) (time.Duration, error)) ([]time.Duration, error) { 21 | if count <= 0 { 22 | return nil, fmt.Errorf("count must be greater than 0") 23 | } 24 | 25 | rttDurations := make([]time.Duration, 0, count) 26 | for i := 1; i <= count; i++ { 27 | rtt, err := pingFunc(ctx, address, timeout) 28 | if err != nil { 29 | continue 30 | } 31 | rttDurations = append(rttDurations, rtt) 32 | if i < count { 33 | time.Sleep(interval) 34 | } 35 | } 36 | 37 | if len(rttDurations) == 0 { 38 | return nil, fmt.Errorf("no successful connections") 39 | } 40 | return rttDurations, nil 41 | } 42 | 43 | func tcpPingFunc(ctx context.Context, address string, timeout time.Duration) (time.Duration, error) { 44 | start := time.Now() 45 | conn, err := net.DialTimeout("tcp", address, timeout) 46 | if err != nil { 47 | return 0, err 48 | } 49 | rtt := time.Since(start) 50 | conn.Close() 51 | return rtt, nil 52 | } 53 | 54 | func httpPingFunc(ctx context.Context, url string, timeout time.Duration) (time.Duration, error) { 55 | // create a custom http client tracing 56 | var ( 57 | onceStart, onceDone sync.Once 58 | connStart time.Time 59 | connDur time.Duration 60 | ) 61 | trace := &httptrace.ClientTrace{ 62 | ConnectStart: func(network, addr string) { 63 | onceStart.Do(func() { 64 | connStart = time.Now() 65 | }) 66 | }, 67 | ConnectDone: func(network, addr string, err error) { 68 | onceDone.Do(func() { 69 | connDur = time.Since(connStart) 70 | }) 71 | }, 72 | } 73 | 74 | // create a http client with timeout and tracing 75 | client := &http.Client{ 76 | Timeout: timeout, 77 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 78 | return http.ErrUseLastResponse // do not follow redirects 79 | }, 80 | Transport: &http.Transport{ 81 | DisableKeepAlives: true, 82 | }, 83 | } 84 | req, err := http.NewRequestWithContext(httptrace.WithClientTrace(ctx, trace), "GET", url, nil) 85 | if err != nil { 86 | return 0, err 87 | } 88 | 89 | // perform the HTTP request 90 | resp, err := client.Do(req) 91 | if err != nil { 92 | return 0, err 93 | } 94 | defer resp.Body.Close() 95 | if resp.StatusCode < 200 || resp.StatusCode >= 400 { 96 | return 0, fmt.Errorf("unacceptable status: %d", resp.StatusCode) 97 | } 98 | return connDur, nil 99 | } 100 | 101 | func createPingStats(address string, count int, rtts []time.Duration) starlark.Value { 102 | vals := make([]float64, len(rtts)) 103 | for i, rtt := range rtts { 104 | vals[i] = float64(rtt) / float64(time.Millisecond) 105 | } 106 | succ := len(rtts) 107 | loss := float64(count-succ) / float64(count) * 100 108 | avg, _ := stats.Mean(vals) 109 | min, _ := stats.Min(vals) 110 | max, _ := stats.Max(vals) 111 | stddev, _ := stats.StandardDeviation(vals) 112 | sd := starlark.StringDict{ 113 | "address": starlark.String(address), 114 | "total": starlark.MakeInt(count), 115 | "success": starlark.MakeInt(succ), 116 | "loss": starlark.Float(loss), 117 | "min": starlark.Float(min), 118 | "avg": starlark.Float(avg), 119 | "max": starlark.Float(max), 120 | "stddev": starlark.Float(stddev), 121 | } 122 | return starlarkstruct.FromStringDict(starlark.String(`statistics`), sd) 123 | } 124 | 125 | func starTCPPing(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 126 | var ( 127 | hostname tps.StringOrBytes 128 | port = 80 129 | count = 4 130 | timeout tps.FloatOrInt = 10 131 | interval tps.FloatOrInt = 1 132 | ) 133 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "hostname", &hostname, "port?", &port, "count?", &count, "timeout?", &timeout, "interval?", &interval); err != nil { 134 | return nil, err 135 | } 136 | 137 | // correct timeout value 138 | if timeout <= 0 { 139 | timeout = 10 140 | } 141 | if interval <= 0 { 142 | interval = 1 143 | } 144 | 145 | // get the context for the DNS lookup and TCP ping 146 | ctx := dataconv.GetThreadContext(thread) 147 | 148 | // resolve the hostname to an IP address 149 | ips, err := goLookup(ctx, hostname.GoString(), "", time.Duration(timeout)*time.Second) 150 | if err != nil { 151 | return none, fmt.Errorf("%s: %w", b.Name(), err) 152 | } 153 | if len(ips) == 0 { 154 | return none, fmt.Errorf("unable to resolve hostname") 155 | } 156 | address := net.JoinHostPort(ips[0], strconv.Itoa(port)) 157 | 158 | // perform the TCP ping, and get the statistics 159 | rtts, err := goPingWrap(ctx, address, count, time.Duration(timeout)*time.Second, time.Duration(interval)*time.Second, tcpPingFunc) 160 | if err != nil { 161 | return none, fmt.Errorf("%s: %w", b.Name(), err) 162 | } 163 | return createPingStats(address, count, rtts), nil 164 | } 165 | 166 | func starHTTPing(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 167 | var ( 168 | url tps.StringOrBytes 169 | count = 4 170 | timeout tps.FloatOrInt = 10 171 | interval tps.FloatOrInt = 1 172 | ) 173 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "url", &url, "count?", &count, "timeout?", &timeout, "interval?", &interval); err != nil { 174 | return nil, err 175 | } 176 | 177 | // correct timeout value 178 | if timeout <= 0 { 179 | timeout = 10 180 | } 181 | if interval <= 0 { 182 | interval = 1 183 | } 184 | 185 | // perform the HTTP ping, and get the statistics 186 | address := url.GoString() 187 | ctx := dataconv.GetThreadContext(thread) 188 | rtts, err := goPingWrap(ctx, address, count, time.Duration(timeout)*time.Second, time.Duration(interval)*time.Second, httpPingFunc) 189 | if err != nil { 190 | return none, fmt.Errorf("%s: %w", b.Name(), err) 191 | } 192 | return createPingStats(address, count, rtts), nil 193 | } 194 | -------------------------------------------------------------------------------- /lib/net/ping_test.go: -------------------------------------------------------------------------------- 1 | package net_test 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "runtime" 7 | "testing" 8 | 9 | itn "github.com/1set/starlet/internal" 10 | "github.com/1set/starlet/lib/net" 11 | "go.starlark.net/starlark" 12 | ) 13 | 14 | // A helper function to create a mock server that returns the specified status code 15 | func createMockServer(statusCode int) *httptest.Server { 16 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | w.WriteHeader(statusCode) 18 | }) 19 | return httptest.NewServer(handler) 20 | } 21 | 22 | // Create a mock server that returns a 301 status code with a Location header 23 | func createRedirectMockServer(location string) *httptest.Server { 24 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | w.Header().Set("Location", location) 26 | w.WriteHeader(http.StatusMovedPermanently) 27 | }) 28 | return httptest.NewServer(handler) 29 | } 30 | 31 | func TestLoadModule_Ping(t *testing.T) { 32 | // create mock servers for testing 33 | server301 := createRedirectMockServer("https://notgoingthere.invalid") 34 | defer server301.Close() 35 | server404 := createMockServer(http.StatusNotFound) 36 | defer server404.Close() 37 | server500 := createMockServer(http.StatusInternalServerError) 38 | defer server500.Close() 39 | 40 | isOnWindows := runtime.GOOS == "windows" 41 | tests := []struct { 42 | name string 43 | script string 44 | wantErr string 45 | skipWindows bool 46 | }{ 47 | // TCPing tests 48 | { 49 | name: `tcping: normal`, 50 | script: itn.HereDoc(` 51 | load('net', 'tcping') 52 | s = tcping('bing.com') 53 | print(s) 54 | assert.eq(s.total, 4) 55 | assert.true(s.success > 0) 56 | `), 57 | }, 58 | { 59 | name: `tcping: abnormal`, 60 | script: itn.HereDoc(` 61 | load('net', 'tcping') 62 | s = tcping('bing.com', count=1, timeout=-5, interval=-2) 63 | print(s) 64 | assert.eq(s.total, 1) 65 | assert.true(s.success > 0) 66 | assert.eq(s.stddev, 0) 67 | `), 68 | }, 69 | { 70 | name: `tcping: faster`, 71 | script: itn.HereDoc(` 72 | load('net', 'tcping') 73 | s = tcping('bing.com', count=10, timeout=5, interval=0.01) 74 | print(s) 75 | assert.eq(s.total, 10) 76 | assert.true(s.success > 0) 77 | `), 78 | }, 79 | { 80 | name: `tcping: not exists`, 81 | script: itn.HereDoc(` 82 | load('net', 'tcping') 83 | s = tcping('missing.invalid') 84 | `), 85 | wantErr: `missing.invalid`, // mac/win: no such host, linux: server misbehaving 86 | }, 87 | { 88 | name: `tcping: wrong count`, 89 | script: itn.HereDoc(` 90 | load('net', 'tcping') 91 | s = tcping('bing.com', count=0) 92 | `), 93 | wantErr: `net.tcping: count must be greater than 0`, 94 | }, 95 | { 96 | name: `tcping: no args`, 97 | script: itn.HereDoc(` 98 | load('net', 'tcping') 99 | tcping() 100 | `), 101 | wantErr: `net.tcping: missing argument for hostname`, 102 | }, 103 | { 104 | name: `tcping: invalid args`, 105 | script: itn.HereDoc(` 106 | load('net', 'tcping') 107 | tcping(123) 108 | `), 109 | wantErr: `net.tcping: for parameter hostname: got int, want string or bytes`, 110 | }, 111 | 112 | // HTTPing tests 113 | { 114 | name: `httping: normal`, 115 | script: itn.HereDoc(` 116 | load('net', 'httping') 117 | s = httping('https://www.bing.com') 118 | print(s) 119 | assert.eq(s.total, 4) 120 | assert.true(s.success > 0) 121 | assert.true(s.min > 0) 122 | `), 123 | }, 124 | { 125 | name: `httping: abnormal`, 126 | script: itn.HereDoc(` 127 | load('net', 'httping') 128 | s = httping('https://www.bing.com', count=1, timeout=-5, interval=-2) 129 | print(s) 130 | assert.eq(s.total, 1) 131 | assert.true(s.success > 0) 132 | assert.eq(s.stddev, 0) 133 | `), 134 | }, 135 | { 136 | name: `httping: faster`, 137 | script: itn.HereDoc(` 138 | load('net', 'httping') 139 | s = httping('https://www.bing.com', count=10, timeout=5, interval=0.01) 140 | print(s) 141 | assert.eq(s.total, 10) 142 | assert.true(s.success > 0) 143 | assert.true(s.min > 0) 144 | `), 145 | }, 146 | { 147 | name: `httping: ignore redirect`, 148 | script: itn.HereDoc(` 149 | load('net', 'httping') 150 | s = httping(server_301, interval=0.1) 151 | assert.eq(s.total, 4) 152 | assert.eq(s.success, 4) 153 | `), 154 | }, 155 | { 156 | name: `httping: status 404`, 157 | script: itn.HereDoc(` 158 | load('net', 'httping') 159 | s = httping(server_404, interval=0.1) 160 | `), 161 | wantErr: `net.httping: no successful connections`, 162 | }, 163 | { 164 | name: `httping: status 500`, 165 | script: itn.HereDoc(` 166 | load('net', 'httping') 167 | s = httping(server_500, interval=0.1) 168 | `), 169 | wantErr: `net.httping: no successful connections`, 170 | }, 171 | { 172 | name: `httping: not exists`, 173 | script: itn.HereDoc(` 174 | load('net', 'httping') 175 | s = httping('http://missing.invalid') 176 | `), 177 | wantErr: `net.httping: no successful connections`, // mac/win: no such host, linux: server misbehaving 178 | }, 179 | { 180 | name: `httping: wrong count`, 181 | script: itn.HereDoc(` 182 | load('net', 'httping') 183 | s = httping('https://www.bing.com', count=0) 184 | `), 185 | wantErr: `net.httping: count must be greater than 0`, 186 | }, 187 | { 188 | name: `httping: no args`, 189 | script: itn.HereDoc(` 190 | load('net', 'httping') 191 | httping() 192 | `), 193 | wantErr: `net.httping: missing argument for url`, 194 | }, 195 | { 196 | name: `httping: invalid args`, 197 | script: itn.HereDoc(` 198 | load('net', 'httping') 199 | httping(123) 200 | `), 201 | wantErr: `net.httping: for parameter url: got int, want string or bytes`, 202 | }, 203 | } 204 | for _, tt := range tests { 205 | t.Run(tt.name, func(t *testing.T) { 206 | if isOnWindows && tt.skipWindows { 207 | t.Skipf("Skip test on Windows") 208 | return 209 | } 210 | extra := starlark.StringDict{ 211 | "server_301": starlark.String(server301.URL), 212 | "server_404": starlark.String(server404.URL), 213 | "server_500": starlark.String(server500.URL), 214 | } 215 | res, err := itn.ExecModuleWithErrorTest(t, net.ModuleName, net.LoadModule, tt.script, tt.wantErr, extra) 216 | if (err != nil) != (tt.wantErr != "") { 217 | t.Errorf("net(%q) expects error = '%v', actual error = '%v', result = %v", tt.name, tt.wantErr, err, res) 218 | return 219 | } 220 | }) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /lib/re/README.md: -------------------------------------------------------------------------------- 1 | # re 2 | 3 | `re` defines regular expression functions, it's intended to be a drop-in subset of [Python's **re** module](https://docs.python.org/3/library/re.html) for Starlark. 4 | 5 | ## Functions 6 | 7 | ### `compile(pattern) Pattern` 8 | 9 | Compile a regular expression pattern into a regular expression object, which 10 | can be used for matching using its match(), search() and other methods. 11 | 12 | #### Parameters 13 | 14 | | name | type | description | 15 | |-----------|----------|-----------------------------------| 16 | | `pattern` | `string` | regular expression pattern string | 17 | 18 | ### `search(pattern,string,flags=0)` 19 | 20 | Scan through string looking for the first location where the regular expression pattern 21 | produces a match, and return a corresponding match object. Return None if no position in 22 | the string matches the pattern; note that this is different from finding a zero-length match 23 | at some point in the string. 24 | 25 | #### Parameters 26 | 27 | | name | type | description | 28 | |-----------|----------|-------------------------------------------------------------------| 29 | | `pattern` | `string` | regular expression pattern string | 30 | | `string` | `string` | input string to search | 31 | | `flags` | `int` | integer flags to control regex behaviour. reserved for future use | 32 | 33 | ### `findall(pattern, text, flags=0)` 34 | 35 | Returns all non-overlapping matches of pattern in string, as a list of strings. 36 | The string is scanned left-to-right, and matches are returned in the order found. 37 | If one or more groups are present in the pattern, return a list of groups; 38 | this will be a list of tuples if the pattern has more than one group. 39 | Empty matches are included in the result. 40 | 41 | #### Parameters 42 | 43 | | name | type | description | 44 | |-----------|----------|-------------------------------------------------------------------| 45 | | `pattern` | `string` | regular expression pattern string | 46 | | `text` | `string` | string to find within | 47 | | `flags` | `int` | integer flags to control regex behaviour. reserved for future use | 48 | 49 | ### `split(pattern, text, maxsplit=0, flags=0)` 50 | 51 | Split text by the occurrences of pattern. If capturing parentheses are used in pattern, 52 | then the text of all groups in the pattern are also returned as part of the resulting list. 53 | If maxsplit is nonzero, at most maxsplit splits occur, and the remainder of the string 54 | is returned as the final element of the list. 55 | 56 | #### Parameters 57 | 58 | | name | type | description | 59 | |------------|----------|-------------------------------------------------------------------| 60 | | `pattern` | `string` | regular expression pattern string | 61 | | `text` | `string` | input string to split | 62 | | `maxsplit` | `int` | maximum number of splits to make. default 0 splits all matches | 63 | | `flags` | `int` | integer flags to control regex behaviour. reserved for future use | 64 | 65 | ### `sub(pattern, repl, text, count=0, flags=0)` 66 | 67 | Return the string obtained by replacing the leftmost non-overlapping occurrences of pattern 68 | in string by the replacement repl. If the pattern isn’t found, string is returned unchanged. 69 | repl can be a string or a function; if it is a string, any backslash escapes in it are processed. 70 | That is, `\n` is converted to a single newline character, `\r` is converted to a carriage return, and so forth. 71 | 72 | #### Parameters 73 | 74 | | name | type | description | 75 | |-----------|----------|---------------------------------------------------------------------| 76 | | `pattern` | `string` | regular expression pattern string | 77 | | `repl` | `string` | string to replace matches with | 78 | | `text` | `string` | input string to replace | 79 | | `count` | `int` | number of replacements to make, default 0 means replace all matches | 80 | | `flags` | `int` | integer flags to control regex behaviour. reserved for future use | 81 | 82 | ### `match(pattern, string, flags=0)` 83 | 84 | If zero or more characters at the beginning of string match the regular expression pattern, 85 | return a corresponding match string tuple. Return None if the string does not match the pattern 86 | 87 | #### Parameters 88 | 89 | | name | type | description | 90 | |-----------|----------|-----------------------------------| 91 | | `pattern` | `string` | regular expression pattern string | 92 | | `string` | `string` | input string to match | 93 | 94 | ## Types 95 | 96 | ### `Pattern` 97 | 98 | **Methods** 99 | 100 | #### `match(text, flags=0)` 101 | 102 | #### `findall(text, flags=0)` 103 | 104 | #### `split(text, maxsplit=0, flags=0)` 105 | 106 | #### `sub(repl, text, count=0, flags=0)` 107 | -------------------------------------------------------------------------------- /lib/re/re_test.go: -------------------------------------------------------------------------------- 1 | package re_test 2 | 3 | import ( 4 | "testing" 5 | 6 | itn "github.com/1set/starlet/internal" 7 | "github.com/1set/starlet/lib/re" 8 | ) 9 | 10 | func TestLoadModule_Re(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | script string 14 | wantErr string 15 | }{ 16 | { 17 | name: `match`, 18 | script: itn.HereDoc(` 19 | load('re', 'match') 20 | match_pattern = r"(\w*)\s*(ADD|REM|DEL|EXT|TRF)\s*(.*)\s*(NAT|INT)\s*(.*)\s*(\(\w{2}\))\s*(.*)" 21 | match_test = "EDM ADD FROM INJURED NAT Jordan BEAULIEU (DB) Western University" 22 | 23 | assert.eq(match(match_pattern,match_test), [(match_test, "EDM", "ADD", "FROM INJURED ", "NAT", "Jordan BEAULIEU ", "(DB)", "Western University")]) 24 | assert.eq(match(match_pattern,"what"), []) 25 | `), 26 | }, 27 | { 28 | name: `match error pattern`, 29 | script: itn.HereDoc(` 30 | load('re', 'match') 31 | match(123, "foo") 32 | `), 33 | wantErr: `re.match: for parameter pattern: got int, want string`, 34 | }, 35 | { 36 | name: `match error string`, 37 | script: itn.HereDoc(` 38 | load('re', 'match') 39 | match("foobar", 2) 40 | `), 41 | wantErr: `re.match: for parameter string: got int, want string`, 42 | }, 43 | { 44 | name: `search`, 45 | script: itn.HereDoc(` 46 | load('re', 'search') 47 | match_pattern = r"(\w*)\s*(ADD|REM|DEL|EXT|TRF)\s*(.*)\s*(NAT|INT)\s*(.*)\s*(\(\w{2}\))\s*(.*)" 48 | match_test = "EDM ADD FROM INJURED NAT Jordan BEAULIEU (DB) Western University" 49 | assert.eq(search(match_pattern, match_test), [0, 64]) 50 | assert.eq(search(match_pattern, "what"), None) 51 | `), 52 | }, 53 | { 54 | name: `search error`, 55 | script: itn.HereDoc(` 56 | load('re', 'search') 57 | search(123, "foo") 58 | `), 59 | wantErr: `re.search: for parameter pattern: got int, want string`, 60 | }, 61 | { 62 | name: `compile`, 63 | script: itn.HereDoc(` 64 | load('re', 'compile') 65 | match_pattern = r"(\w*)\s*(ADD|REM|DEL|EXT|TRF)\s*(.*)\s*(NAT|INT)\s*(.*)\s*(\(\w{2}\))\s*(.*)" 66 | match_test = "EDM ADD FROM INJURED NAT Jordan BEAULIEU (DB) Western University" 67 | 68 | match_r = compile(match_pattern) 69 | assert.eq(match_r.match(match_test), [(match_test, "EDM", "ADD", "FROM INJURED ", "NAT", "Jordan BEAULIEU ", "(DB)", "Western University")]) 70 | assert.eq(match_r.match("ab acdef"), []) 71 | assert.eq(match_r.sub("", match_test), "") 72 | `), 73 | }, 74 | { 75 | name: `compile error`, 76 | script: itn.HereDoc(` 77 | load('re', 'compile') 78 | compile(123) 79 | `), 80 | wantErr: `re.compile: for parameter pattern: got int, want string`, 81 | }, 82 | { 83 | name: `compile fail`, 84 | script: itn.HereDoc(` 85 | load('re', 'compile') 86 | compile("\q") 87 | `), 88 | wantErr: `re_test.star:3:9: invalid escape sequence \q`, 89 | }, 90 | { 91 | name: `sub`, 92 | script: itn.HereDoc(` 93 | load('re', 'sub') 94 | match_pattern = r"(\w*)\s*(ADD|REM|DEL|EXT|TRF)\s*(.*)\s*(NAT|INT)\s*(.*)\s*(\(\w{2}\))\s*(.*)" 95 | match_test = "EDM ADD FROM INJURED NAT Jordan BEAULIEU (DB) Western University" 96 | 97 | assert.eq(sub(match_pattern, "", match_test), "") 98 | assert.eq(sub(match_pattern, "", "ab acdef"), "ab acdef") 99 | `), 100 | }, 101 | { 102 | name: `sub error`, 103 | script: itn.HereDoc(` 104 | load('re', 'sub') 105 | sub(123, "", "foo") 106 | `), 107 | wantErr: `re.sub: for parameter pattern: got int, want string`, 108 | }, 109 | { 110 | name: `split`, 111 | script: itn.HereDoc(` 112 | load('re', 'split', 'compile') 113 | space_r = compile(" ") 114 | assert.eq(split(" ", "foo bar baz bat"), ("foo", "bar", "baz", "bat")) 115 | assert.eq(space_r.split("foo bar baz bat"), ("foo", "bar", "baz", "bat")) 116 | assert.eq(split(" ", "foobar"), ("foobar",)) 117 | `), 118 | }, 119 | { 120 | name: `split error`, 121 | script: itn.HereDoc(` 122 | load('re', 'split') 123 | split(123, "foo") 124 | `), 125 | wantErr: `re.split: for parameter pattern: got int, want string`, 126 | }, 127 | { 128 | name: `findall`, 129 | script: itn.HereDoc(` 130 | load('re', 'compile', 'findall') 131 | foo_r = compile("foo") 132 | assert.eq(findall("foo", "foo bar baz"), ("foo",)) 133 | assert.eq(foo_r.findall("foo bar baz"), ("foo",)) 134 | assert.eq(findall("foo", "bar baz"), ()) 135 | `), 136 | }, 137 | { 138 | name: `findall error`, 139 | script: itn.HereDoc(` 140 | load('re', 'findall') 141 | findall(123, "foo") 142 | `), 143 | wantErr: `re.findall: for parameter pattern: got int, want string`, 144 | }, 145 | } 146 | for _, tt := range tests { 147 | t.Run(tt.name, func(t *testing.T) { 148 | res, err := itn.ExecModuleWithErrorTest(t, re.ModuleName, re.LoadModule, tt.script, tt.wantErr, nil) 149 | if (err != nil) != (tt.wantErr != "") { 150 | t.Errorf("re(%q) expects error = '%v', actual error = '%v', result = %v", tt.name, tt.wantErr, err, res) 151 | return 152 | } 153 | }) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /lib/runtime/README.md: -------------------------------------------------------------------------------- 1 | # runtime 2 | 3 | `runtime` is a Starlark module provides Go and app runtime information. 4 | 5 | ## Constants 6 | 7 | - `hostname`: A string representing the hostname of the system where the script is being executed. 8 | - `workdir`: A string representing the current working directory of the process. 9 | - `homedir`: A string representing the home directory of the user running the process, it's `$HOME` on Unix/Linux, `%USERPROFILE%` on Windows. 10 | - `os`: A string representing the operating system of the runtime. This value comes from Go's `runtime.GOOS`. 11 | - `arch`: A string representing the architecture of the machine. This value is derived from Go's `runtime.GOARCH`. 12 | - `gover`: A string representing the Go runtime version. This is obtained using `runtime.Version()` from the Go standard library. 13 | - `pid`: An integer representing the process ID of the current process. 14 | - `ppid`: An integer representing the parent process ID of the current process. 15 | - `uid`: An integer representing the user ID of the process owner. 16 | - `gid`: An integer representing the group ID of the process owner. 17 | - `app_start`: A time value representing the moment when the application started. This is used to calculate uptime. 18 | 19 | ## Functions 20 | 21 | ### `uptime()` 22 | 23 | Returns the uptime of the current process in `time.duration`. 24 | 25 | #### Examples 26 | 27 | **basic** 28 | 29 | Returns the uptime of the current process immediately. 30 | 31 | ```python 32 | load("runtime", "uptime") 33 | print(uptime()) 34 | # Output: 883.583µs 35 | ``` 36 | 37 | ### `getenv(key, default=None)` 38 | 39 | Returns the value of the environment variable key as a string if it exists, or default if it doesn't. 40 | 41 | #### Examples 42 | 43 | **basic** 44 | 45 | Returns the value of the environment variable PATH if it exists, or None if it doesn't. 46 | 47 | ```python 48 | load("runtime", "getenv") 49 | print(getenv("PATH")) 50 | # Output: /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin 51 | ``` 52 | 53 | ### `putenv(key, value)` 54 | 55 | Sets the value of the environment variable named by the key, returning an error if any. 56 | 57 | #### Examples 58 | 59 | **basic** 60 | 61 | Sets the environment variable `STARLET_TEST` to the value `123456`. 62 | 63 | ```python 64 | load("runtime", "putenv") 65 | putenv("STARLET_TEST", 123456) 66 | ``` 67 | 68 | ### `setenv(key, value)` 69 | 70 | Sets the value of the environment variable named by the key, returning an error if any. 71 | Alias of `putenv`. 72 | 73 | #### Examples 74 | 75 | **basic** 76 | 77 | Sets the environment variable `STARLET_TEST` to the value `ABC`. 78 | 79 | ```python 80 | load("runtime", "setenv") 81 | setenv("STARLET_TEST", "ABC") 82 | ``` 83 | 84 | ### `unsetenv(key)` 85 | 86 | Unsets a single environment variable. 87 | 88 | #### Examples 89 | 90 | **basic** 91 | 92 | Unsets the environment variable STARLET_TEST. 93 | 94 | ```python 95 | load("runtime", "unsetenv") 96 | unsetenv("STARLET_TEST") 97 | ``` 98 | -------------------------------------------------------------------------------- /lib/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | // Package runtime implements the Starlark module for Go and app runtime information. 2 | package runtime 3 | 4 | import ( 5 | "os" 6 | grt "runtime" 7 | "sync" 8 | "time" 9 | 10 | "github.com/1set/starlet/dataconv" 11 | stdtime "go.starlark.net/lib/time" 12 | "go.starlark.net/starlark" 13 | "go.starlark.net/starlarkstruct" 14 | ) 15 | 16 | // ModuleName defines the expected name for this Module when used in starlark's load() function, eg: load('base64', 'encode') 17 | const ModuleName = "runtime" 18 | 19 | var ( 20 | once sync.Once 21 | moduleData starlark.StringDict 22 | ) 23 | 24 | // LoadModule loads the runtime module. It is concurrency-safe and idempotent. 25 | func LoadModule() (md starlark.StringDict, err error) { 26 | once.Do(func() { 27 | var host, pwd, hd string 28 | if host, err = os.Hostname(); err != nil { 29 | return 30 | } 31 | if pwd, err = os.Getwd(); err != nil { 32 | return 33 | } 34 | if hd, err = os.UserHomeDir(); err != nil { 35 | return 36 | } 37 | moduleData = starlark.StringDict{ 38 | ModuleName: &starlarkstruct.Module{ 39 | Name: ModuleName, 40 | Members: starlark.StringDict{ 41 | "hostname": starlark.String(host), 42 | "workdir": starlark.String(pwd), 43 | "homedir": starlark.String(hd), 44 | "os": starlark.String(grt.GOOS), 45 | "arch": starlark.String(grt.GOARCH), 46 | "gover": starlark.String(grt.Version()), 47 | "pid": starlark.MakeInt(os.Getpid()), 48 | "ppid": starlark.MakeInt(os.Getppid()), 49 | "uid": starlark.MakeInt(os.Getuid()), 50 | "gid": starlark.MakeInt(os.Getgid()), 51 | "app_start": stdtime.Time(appStart), 52 | "uptime": starlark.NewBuiltin(ModuleName+".uptime", getUpTime), 53 | "getenv": starlark.NewBuiltin(ModuleName+".getenv", getenv), 54 | "putenv": starlark.NewBuiltin(ModuleName+".putenv", putenv), 55 | "setenv": starlark.NewBuiltin(ModuleName+".setenv", putenv), // alias "setenv" to "putenv" 56 | "unsetenv": starlark.NewBuiltin(ModuleName+".unsetenv", unsetenv), 57 | }, 58 | }, 59 | } 60 | }) 61 | return moduleData, err 62 | } 63 | 64 | var ( 65 | appStart = time.Now() 66 | ) 67 | 68 | // getUpTime returns time elapsed since the app started. 69 | func getUpTime(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 70 | if err := starlark.UnpackPositionalArgs(b.Name(), args, kwargs, 0); err != nil { 71 | return nil, err 72 | } 73 | return stdtime.Duration(time.Since(appStart)), nil 74 | } 75 | 76 | // getenv returns the value of the environment variable key as a string if it exists, or default if it doesn't. 77 | func getenv(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 78 | var ( 79 | key string 80 | defVal starlark.Value = starlark.None 81 | ) 82 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "key", &key, "default?", &defVal); err != nil { 83 | return nil, err 84 | } 85 | // get the value 86 | if val, ok := os.LookupEnv(key); ok { 87 | return starlark.String(val), nil 88 | } 89 | return defVal, nil 90 | } 91 | 92 | // putenv sets the value of the environment variable named by the key, returning an error if any. 93 | // value should be a string, or it will be converted to a string. 94 | func putenv(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 95 | var ( 96 | key string 97 | val starlark.Value 98 | ) 99 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "key", &key, "value", &val); err != nil { 100 | return nil, err 101 | } 102 | // set the value 103 | err := os.Setenv(key, dataconv.StarString(val)) 104 | return starlark.None, err 105 | } 106 | 107 | // unsetenv unsets a single environment variable. 108 | func unsetenv(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { 109 | var key string 110 | if err := starlark.UnpackArgs(b.Name(), args, kwargs, "key", &key); err != nil { 111 | return nil, err 112 | } 113 | // unset the value 114 | err := os.Unsetenv(key) 115 | return starlark.None, err 116 | } 117 | -------------------------------------------------------------------------------- /lib/runtime/runtime_test.go: -------------------------------------------------------------------------------- 1 | package runtime_test 2 | 3 | import ( 4 | "testing" 5 | 6 | itn "github.com/1set/starlet/internal" 7 | rt "github.com/1set/starlet/lib/runtime" 8 | ) 9 | 10 | func TestLoadModule_Runtime(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | script string 14 | wantErr string 15 | }{ 16 | { 17 | name: `host`, 18 | script: itn.HereDoc(` 19 | load('runtime', 'hostname', 'workdir', 'homedir', 'os', 'arch', 'gover') 20 | ss = [hostname, workdir, homedir, os, arch, gover] 21 | print(ss) 22 | _ = [assert.eq(type(s), "string") for s in ss] 23 | `), 24 | }, 25 | { 26 | name: `process`, 27 | script: itn.HereDoc(` 28 | load('runtime', 'pid', 'ppid', 'uid', 'gid') 29 | si = [pid, ppid, uid, gid] 30 | print(si) 31 | _ = [assert.eq(type(s), "int") for s in si] 32 | `), 33 | }, 34 | { 35 | name: `app`, 36 | script: itn.HereDoc(` 37 | load('runtime', s='app_start', ut='uptime') 38 | assert.eq(type(s), "time.time") 39 | u = ut() 40 | assert.eq(type(u), "time.duration") 41 | print(s, u) 42 | `), 43 | }, 44 | { 45 | name: `uptime invalid`, 46 | script: itn.HereDoc(` 47 | load('runtime', 'uptime') 48 | uptime(123) 49 | `), 50 | wantErr: `runtime.uptime: got 1 arguments, want 0`, 51 | }, 52 | { 53 | name: `getenv: no args`, 54 | script: itn.HereDoc(` 55 | load('runtime', 'getenv') 56 | getenv() 57 | `), 58 | wantErr: `runtime.getenv: missing argument for key`, 59 | }, 60 | { 61 | name: `getenv: invalid`, 62 | script: itn.HereDoc(` 63 | load('runtime', 'getenv') 64 | getenv(123) 65 | `), 66 | wantErr: `runtime.getenv: for parameter key: got int, want string`, 67 | }, 68 | { 69 | name: `getenv: no result`, 70 | script: itn.HereDoc(` 71 | load('runtime', 'getenv') 72 | x = getenv("very-long-long-non-existent") 73 | assert.eq(x, None) 74 | y = getenv("very-long-long-non-existent", 1000) 75 | assert.eq(y, 1000) 76 | `), 77 | }, 78 | { 79 | name: `getenv: with result`, 80 | script: itn.HereDoc(` 81 | load('runtime', 'getenv') 82 | x = getenv("PATH") 83 | print("PATH:", x) 84 | assert.eq(type(x), "string") 85 | `), 86 | }, 87 | { 88 | name: `putenv: no args`, 89 | script: itn.HereDoc(` 90 | load('runtime', 'putenv') 91 | putenv() 92 | `), 93 | wantErr: `runtime.putenv: missing argument for key`, 94 | }, 95 | { 96 | name: `putenv: invalid`, 97 | script: itn.HereDoc(` 98 | load('runtime', 'putenv') 99 | putenv(123, "value") 100 | `), 101 | wantErr: `runtime.putenv: for parameter key: got int, want string`, 102 | }, 103 | { 104 | name: `putenv: no value`, 105 | script: itn.HereDoc(` 106 | load('runtime', 'putenv') 107 | putenv("key") 108 | `), 109 | wantErr: `runtime.putenv: missing argument for value`, 110 | }, 111 | { 112 | name: `putenv: new value`, 113 | script: itn.HereDoc(` 114 | load('runtime', 'putenv', 'getenv') 115 | putenv("STARLET_TEST", 123456) 116 | x = getenv("STARLET_TEST") 117 | print("STARLET_TEST:", x) 118 | assert.eq(x, "123456") 119 | `), 120 | }, 121 | { 122 | name: `putenv: existing value`, 123 | script: itn.HereDoc(` 124 | load('runtime', 'putenv', 'getenv') 125 | putenv("STARLET_TEST", 123456) 126 | putenv("STARLET_TEST", 654321) 127 | x = getenv("STARLET_TEST") 128 | print("STARLET_TEST:", x) 129 | assert.eq(x, "654321") 130 | `), 131 | }, 132 | { 133 | name: `unsetenv: no args`, 134 | script: itn.HereDoc(` 135 | load('runtime', 'unsetenv') 136 | unsetenv() 137 | `), 138 | wantErr: `runtime.unsetenv: missing argument for key`, 139 | }, 140 | { 141 | name: `unsetenv: invalid`, 142 | script: itn.HereDoc(` 143 | load('runtime', 'unsetenv') 144 | unsetenv(123) 145 | `), 146 | wantErr: `runtime.unsetenv: for parameter key: got int, want string`, 147 | }, 148 | { 149 | name: `unsetenv: non-existent`, 150 | script: itn.HereDoc(` 151 | load('runtime', 'unsetenv') 152 | unsetenv("very-long-long-non-existent") 153 | `), 154 | }, 155 | { 156 | name: `unsetenv: existing`, 157 | script: itn.HereDoc(` 158 | load('runtime', 'putenv', 'unsetenv', 'getenv') 159 | putenv("STARLET_TEST", 123456) 160 | x = getenv("STARLET_TEST") 161 | print("STARLET_TEST:", x) 162 | assert.eq(x, "123456") 163 | unsetenv("STARLET_TEST") 164 | y = getenv("STARLET_TEST") 165 | print("STARLET_TEST:", y) 166 | assert.eq(y, None) 167 | `), 168 | }, 169 | { 170 | name: `setenv like putenv`, 171 | script: itn.HereDoc(` 172 | load('runtime', 'setenv', 'getenv') 173 | setenv("STARLET_TEST", 123456) 174 | x = getenv("STARLET_TEST") 175 | print("STARLET_TEST:", x) 176 | assert.eq(x, "123456") 177 | `), 178 | }, 179 | } 180 | for _, tt := range tests { 181 | t.Run(tt.name, func(t *testing.T) { 182 | res, err := itn.ExecModuleWithErrorTest(t, rt.ModuleName, rt.LoadModule, tt.script, tt.wantErr, nil) 183 | if (err != nil) != (tt.wantErr != "") { 184 | t.Errorf("runtime(%q) expects error = '%v', actual error = '%v', result = %v", tt.name, tt.wantErr, err, res) 185 | return 186 | } 187 | }) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /testdata/aloha.star: -------------------------------------------------------------------------------- 1 | print("Aloha, Honua!") 2 | -------------------------------------------------------------------------------- /testdata/calc.star: -------------------------------------------------------------------------------- 1 | load("fibonacci.star", "fibonacci", fib = "fibonacci") 2 | 3 | fibonacci = 123 4 | x = fibonacci * 2 5 | print("Z", x) 6 | 7 | f = fibonacci(10) 8 | print("A", f[-1]) 9 | print("B", fib(10)[-1]) 10 | -------------------------------------------------------------------------------- /testdata/circle1.star: -------------------------------------------------------------------------------- 1 | load("circle2.star", "work") 2 | 3 | def life(): 4 | return "la vie" 5 | -------------------------------------------------------------------------------- /testdata/circle2.star: -------------------------------------------------------------------------------- 1 | load("circle1.star", "life") 2 | 3 | def work(): 4 | return "le travail" 5 | -------------------------------------------------------------------------------- /testdata/coins.star: -------------------------------------------------------------------------------- 1 | coins = { 2 | "dime": 10, 3 | "nickel": 5, 4 | "penny": 1, 5 | "quarter": 25, 6 | } 7 | 8 | print("By name:\t" + ", ".join(sorted(coins.keys()))) 9 | print("By value:\t" + ", ".join(sorted(coins.keys(), key = coins.get))) 10 | -------------------------------------------------------------------------------- /testdata/empty.star: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1set/starlet/bb483ca9cdfb1d68e9b39705a28a314375ee2405/testdata/empty.star -------------------------------------------------------------------------------- /testdata/factorial.star: -------------------------------------------------------------------------------- 1 | def factorial(n): 2 | if n < 0: 3 | return None 4 | result = 1 5 | for i in range(2, n + 1): 6 | result *= i 7 | return result 8 | -------------------------------------------------------------------------------- /testdata/fibonacci.star: -------------------------------------------------------------------------------- 1 | def fibonacci(n): 2 | res = list(range(n + 1)) 3 | for i in res[2:]: 4 | res[i] = res[i - 2] + res[i - 1] 5 | return res[1:] 6 | 7 | def fib_last(n): 8 | return fibonacci(n)[-1] 9 | 10 | # print("tf", fibonacci(100)) 11 | -------------------------------------------------------------------------------- /testdata/fibonacci2.star: -------------------------------------------------------------------------------- 1 | def fib(n): 2 | if n < 2: 3 | return n 4 | return fib(n - 1) + fib(n - 2) 5 | -------------------------------------------------------------------------------- /testdata/magic.star: -------------------------------------------------------------------------------- 1 | def answer(): 2 | return 42 3 | 4 | def custom(): 5 | return "Custom[{}]".format(magic_number) 6 | 7 | # print("C", custom()) 8 | -------------------------------------------------------------------------------- /testdata/nemo/two.star: -------------------------------------------------------------------------------- 1 | a = 200 2 | -------------------------------------------------------------------------------- /testdata/one.star: -------------------------------------------------------------------------------- 1 | number = input * 100 2 | -------------------------------------------------------------------------------- /testdata/two.star: -------------------------------------------------------------------------------- 1 | a = 2 2 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package starlet 2 | 3 | import "go.starlark.net/starlark" 4 | 5 | // StringAnyMap type is a map of string to interface{} and is used to store global variables like StringDict of Starlark, but not a Starlark type. 6 | type StringAnyMap map[string]interface{} 7 | 8 | // Clone returns a copy of the data store. It returns an empty map if the current data store is nil. 9 | func (d StringAnyMap) Clone() StringAnyMap { 10 | clone := make(StringAnyMap) 11 | for k, v := range d { 12 | clone[k] = v 13 | } 14 | return clone 15 | } 16 | 17 | // Merge merges the given data store into the current data store. It does nothing if the current data store is nil. 18 | func (d StringAnyMap) Merge(other StringAnyMap) { 19 | if d == nil { 20 | return 21 | } 22 | for k, v := range other { 23 | d[k] = v 24 | } 25 | } 26 | 27 | // MergeDict merges the given string dict into the current data store. 28 | func (d StringAnyMap) MergeDict(other starlark.StringDict) { 29 | if d == nil { 30 | return 31 | } 32 | for k, v := range other { 33 | d[k] = v 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /types_test.go: -------------------------------------------------------------------------------- 1 | package starlet 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "go.starlark.net/starlark" 8 | ) 9 | 10 | func TestStringAnyMap_Clone(t *testing.T) { 11 | tests := []struct { 12 | name string 13 | d StringAnyMap 14 | want StringAnyMap 15 | }{ 16 | { 17 | name: "test_nil_map_clone", 18 | d: nil, 19 | want: make(StringAnyMap), 20 | }, 21 | { 22 | name: "test_empty_map_clone", 23 | d: make(StringAnyMap), 24 | want: make(StringAnyMap), 25 | }, 26 | { 27 | name: "test_non_empty_map_clone", 28 | d: StringAnyMap{"key1": "val1", "key2": 2, "key3": true}, 29 | want: StringAnyMap{"key1": "val1", "key2": 2, "key3": true}, 30 | }, 31 | } 32 | 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | if got := tt.d.Clone(); !reflect.DeepEqual(got, tt.want) { 36 | t.Errorf("Clone() = %v, want %v", got, tt.want) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestStringAnyMap_Merge(t *testing.T) { 43 | tests := []struct { 44 | name string 45 | d StringAnyMap 46 | other StringAnyMap 47 | want StringAnyMap 48 | wantEffect bool 49 | }{ 50 | { 51 | name: "test_nil_merge", 52 | d: nil, 53 | other: StringAnyMap{"key1": "val1"}, 54 | wantEffect: false, 55 | }, 56 | { 57 | name: "test_merge_map", 58 | d: StringAnyMap{"key1": "val1", "key2": 2}, 59 | other: StringAnyMap{"key2": "val2", "key3": true}, 60 | want: StringAnyMap{"key1": "val1", "key2": "val2", "key3": true}, 61 | wantEffect: true, 62 | }, 63 | } 64 | 65 | for _, tt := range tests { 66 | t.Run(tt.name, func(t *testing.T) { 67 | orig := tt.d.Clone() 68 | if tt.d == nil { 69 | orig = nil 70 | } 71 | // do the merge and check the result 72 | tt.d.Merge(tt.other) 73 | if tt.wantEffect && !reflect.DeepEqual(tt.d, tt.want) { 74 | t.Errorf("Merge() got = %v, want %v", tt.d, tt.want) 75 | } else if !tt.wantEffect && !reflect.DeepEqual(tt.d, orig) { 76 | t.Errorf("Merge() got = %v, want original map: %v", tt.d, orig) 77 | } 78 | }) 79 | } 80 | } 81 | 82 | // Assuming starlark.StringDict is equivalent to map[string]interface{} for this example. 83 | func TestStringAnyMap_MergeDict(t *testing.T) { 84 | tests := []struct { 85 | name string 86 | d StringAnyMap 87 | other starlark.StringDict 88 | want StringAnyMap 89 | wantEffect bool 90 | }{ 91 | { 92 | name: "test_nil_merge_dict", 93 | d: nil, 94 | other: starlark.StringDict{"key1": starlark.String("val1")}, 95 | wantEffect: false, 96 | }, 97 | { 98 | name: "test_merge_dict", 99 | d: StringAnyMap{"key1": "val1", "key2": 2}, 100 | other: starlark.StringDict{"key2": starlark.String("val2"), "key3": starlark.Bool(true)}, 101 | want: StringAnyMap{"key1": "val1", "key2": starlark.String("val2"), "key3": starlark.Bool(true)}, 102 | wantEffect: true, 103 | }, 104 | } 105 | 106 | for _, tt := range tests { 107 | t.Run(tt.name, func(t *testing.T) { 108 | orig := tt.d.Clone() 109 | if tt.d == nil { 110 | orig = nil 111 | } 112 | // do the merge and check the result 113 | tt.d.MergeDict(tt.other) 114 | if tt.wantEffect && !reflect.DeepEqual(tt.d, tt.want) { 115 | t.Errorf("MergeDict() got = %v, want %v", tt.d, tt.want) 116 | } else if !tt.wantEffect && !reflect.DeepEqual(tt.d, orig) { 117 | t.Errorf("MergeDict() got = %v, want original map: %v", tt.d, orig) 118 | } 119 | }) 120 | } 121 | } 122 | --------------------------------------------------------------------------------