Skip to content

Commit

Permalink
Initial code
Browse files Browse the repository at this point in the history
  • Loading branch information
lestrrat committed Aug 21, 2024
1 parent 0d21783 commit 638fc3f
Show file tree
Hide file tree
Showing 15 changed files with 615 additions and 0 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: CI
on:
- push
- pull_request

jobs:
build:
runs-on: ubuntu-latest
name: Test
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Install Go stable version
uses: actions/setup-go@v5
with:
go-version: "1.22"
- name: Run tests
run: go test ./...

15 changes: 15 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: lint
on: [push]
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- uses: golangci/golangci-lint-action@v6
with:
version: v1.60
args: --timeout=5m
89 changes: 89 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
run:

linters-settings:
govet:
enable-all: true
disable:
- shadow
- fieldalignment

linters:
enable-all: true
disable:
- contextcheck
- cyclop
- depguard
- dupl
- exhaustive
- exhaustruct
- err113
- errorlint
- funlen
- gci
- gochecknoglobals
- gochecknoinits
- gocognit
- gocritic
- gocyclo
- godot
- godox
- gofumpt
- gomnd
- gomoddirectives # I think it's broken
- gosec
- gosmopolitan
- govet
- inamedparam # oh, sod off
- interfacebloat
- ireturn # No, I _LIKE_ returning interfaces
- lll
- maintidx # Do this in code review
- makezero
- mnd # TODO: re-enable when we can check again
- nonamedreturns
- nakedret
- nestif
- nlreturn
- paralleltest
- perfsprint
- testifylint # TODO: re-enable when we can check again
- tagliatelle
- testpackage
- thelper # Tests are fine
- varnamelen # Short names are ok
- wrapcheck
- wsl

issues:
exclude-rules:
# not needed
- path: /*.go
text: "ST1003: should not use underscores in package names"
linters:
- stylecheck
- path: /*.go
text: "don't use an underscore in package name"
linters:
- revive
- path: /main.go
linters:
- errcheck
- path: /*_test.go
linters:
- errcheck
- errchkjson
- forcetypeassert
- path: /*_example_test.go
linters:
- forbidigo
- path: /*_test.go
text: "var-naming: "
linters:
- revive

# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0

# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0

60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# scriptor

A "scripting" framework, to coordinate actions: For example, this can be useful to construct testing sequences in Go.

# HOW TO USE

## 1. Create a `scene.Scene` object

```go
s := scene.New()
```

## 2. Add `scene.Action` objects to `scene.Scene`

```go
// inline Action
s.Add(scene.ActionFunc(func(ctx context.Context) error {
// .. do something ...
return nil
}))

// built-in Action
s.Add(actions.Delay(5*time.Second))

// custom Action object
s.Add(myCustomAction{})
```

## 3. Prepare a `context.Context` object

All transient data in this tool is expected to be passed down along with a `context.Context` object.

For example, logging is done through a `slog.Logger` passed down with the context. This means that the `context.Context` object must be primed with the logger object before the `Action`s are fired.

To do this, you can manually create a context object using the appropriate injector functions, such as `log.InjectContext()`:

```go
ctx := log.InjectContext(context.Background(), slog.New(....))
```

Or, to get the default set of values injected, you can use `scriptor.DefaultContext()`:

```go
ctx := scriptor.DefaultContext(context.Background())
```

The values that need to be injected defer based on the actions that you provide.
As of this writing the documentation is lacking, but in the future each component
in this module should clearly state which values need to be injected into the
`context.Context` object.

As of this writing it is safest to just use `scriptor.DefaultContext()` for most everything.

## 4. Execute the `scene.Scene` object

Finally, put everything together and execute the scene.

```go
s.Execute(ctx)
```
23 changes: 23 additions & 0 deletions actions/delay.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package actions

import (
"context"
"time"

"github.com/lestrrat-go/scriptor/scene"
)

type delay time.Duration

func (d delay) Execute(ctx context.Context) error {
select {
case <-time.After(time.Duration(d)):
return nil
case <-ctx.Done():
return ctx.Err()
}
}

func Delay(d time.Duration) scene.Action {
return delay(d)
}
50 changes: 50 additions & 0 deletions clock/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package clock

import (
"context"
"time"
)

// Clock is an interface that provides the current time.
type Clock interface {
Now() time.Time
}

type static time.Time

func Static(t time.Time) Clock {
return static(t)
}

func (s static) Now() time.Time {
return time.Time(s)
}

type realClock struct{}

func (realClock) Now() time.Time {
return time.Now()
}

func RealClock() Clock {
return realClock{}
}

type clockKey struct{}

func InjectContext(ctx context.Context, v Clock) context.Context {
return context.WithValue(ctx, clockKey{}, v)
}

// FromContext returns the Clock from the context, if any.
// Make sure to check the return value for nil.
func FromContext(ctx context.Context) Clock {
v := ctx.Value(clockKey{})
if v == nil {
return nil
}
if c, ok := v.(Clock); ok {
return c
}
return nil
}
11 changes: 11 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module github.com/lestrrat-go/scriptor

go 1.22.6

require github.com/stretchr/testify v1.9.0

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
92 changes: 92 additions & 0 deletions httpactions/httpactions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package httpactions

import (
"context"
"fmt"
"log/slog"
"net/http"

"github.com/lestrrat-go/scriptor/log"
"github.com/lestrrat-go/scriptor/scene"
"github.com/lestrrat-go/scriptor/stash"
)

type prevRequestKey struct{}
type prevResponseKey struct{}

func PrevRequest(ctx context.Context) *http.Request {
st := stash.FromContext(ctx)
if st == nil {
return nil
}

v, ok := st.Get(prevRequestKey{})
if !ok {
return nil
}

if req, ok := v.(*http.Request); ok {
return req
}
return nil
}

func PrevResponse(ctx context.Context) *http.Response {
st := stash.FromContext(ctx)
if st == nil {
return nil
}

v, ok := st.Get(prevResponseKey{})
if !ok {
return nil
}

if res, ok := v.(*http.Response); ok {
return res
}
return nil
}

type Client struct {
client *http.Client
}

func NewClient(client *http.Client) *Client {
return &Client{client: client}
}

func (h *Client) GetAction(u string) scene.Action {
r, err := http.NewRequest(http.MethodGet, u, nil)
return &httpAction{client: h.client, req: r, err: err, name: "GetAction"}
}

type httpAction struct {
client *http.Client
req *http.Request
err error
name string
}

func (h *httpAction) Execute(ctx context.Context) error {
logger := log.FromContext(ctx)
if err := h.err; err != nil {
logger.LogAttrs(ctx, slog.LevelInfo, "actions.httpAction", slog.String("error", err.Error()))
return fmt.Errorf("actions.httpAction: error during setup: %w", err)
}

req := h.req.WithContext(ctx)
//nolint:bodyclose
res, err := h.client.Do(req)
if err != nil {
return fmt.Errorf("actions.httpAction: error during request: %w", err)
}

st := stash.FromContext(ctx)

logger.DebugContext(ctx, "actions.httpAction", slog.Any("request", req))
logger.DebugContext(ctx, "actions.httpAction", slog.Any("response", res))
st.Set(prevRequestKey{}, req)
st.Set(prevResponseKey{}, res)
return nil
}
Loading

0 comments on commit 638fc3f

Please sign in to comment.