├── .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 |
--------------------------------------------------------------------------------