Skip to content

Commit

Permalink
Add the slangroom go package (#4)
Browse files Browse the repository at this point in the history
* feat: add go bindings package

* fix readme

* fix readme

* fix: naming. remove executable. add vendor

---------

Co-authored-by: puria <[email protected]>
  • Loading branch information
FilippoTrotter and puria authored Oct 9, 2024
1 parent 004ebda commit e9ca9a1
Show file tree
Hide file tree
Showing 342 changed files with 181,347 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
slangroom-exec
slangroom-exec-*
bindings/go/slangroom-exec
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore

# Logs
Expand Down
25 changes: 25 additions & 0 deletions bindings/go/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Top-most EditorConfig file
root = true

# Every file should according to these default configurations if not specified
[*]
# Use UNIX-style line endings
end_of_line = LF
# Use utf-8 file encoding
charset = utf-8
# 4 space indent
indent_style = space
indent_size = 4
# Ensure file ends with a newline when saving(prevent `no newline at EOF`)
insert_final_newline = true
# Remove any whitespace characters preceding newline characters
trim_trailing_whitespace = true

# For YAML
[*.{yml,yaml}]
indent_size = 2

# For Go files
[*.go]
# `gofmt` uses tabs for indentation
indent_style = tab
104 changes: 104 additions & 0 deletions bindings/go/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Based on a true story on:
# https://gist.github.com/thomaspoignant/5b72d579bd5f311904d973652180c705

GOCMD=go
GOTEST=$(GOCMD) test
GOVET=$(GOCMD) vet
BINARY_NAME=slangroom_go
VERSION?=0.0.0
SERVICE_PORT?=3000
DOCKER_REGISTRY?= #if set it should finished by /
EXPORT_RESULT?=false # for CI please set EXPORT_RESULT to true

GREEN := $(shell tput -Txterm setaf 2)
YELLOW := $(shell tput -Txterm setaf 3)
WHITE := $(shell tput -Txterm setaf 7)
CYAN := $(shell tput -Txterm setaf 6)
RESET := $(shell tput -Txterm sgr0)

.PHONY: all test build vendor

all: help

slangroom-exec:
cd ../.. && make slangroom-exec && cp slangroom-exec bindings/go

## Build:
build: vendor slangroom-exec ## Build your project and put the output binary in out/bin/
mkdir -p out/bin
GO111MODULE=on $(GOCMD) build -mod vendor -o out/bin/$(BINARY_NAME) .

clean: ## Remove build related file
rm -fr ./bin
rm -fr ./out
rm -f ./junit-report.xml checkstyle-report.xml ./coverage.xml ./profile.cov yamllint-checkstyle.xml

vendor: ## Copy of all packages needed to support builds and tests in the vendor directory
$(GOCMD) mod vendor

watch: ## Run the code with cosmtrek/air to have automatic reload on changes
$(eval PACKAGE_NAME=$(shell head -n 1 go.mod | cut -d ' ' -f2))
docker run -it --rm -w /go/src/$(PACKAGE_NAME) -v $(shell pwd):/go/src/$(PACKAGE_NAME) -p $(SERVICE_PORT):$(SERVICE_PORT) cosmtrek/air

## Test:
test: ## Run the tests of the project
ifeq ($(EXPORT_RESULT), true)
GO111MODULE=off go get -u github.com/jstemmer/go-junit-report
$(eval OUTPUT_OPTIONS = | tee /dev/tty | go-junit-report -set-exit-code > junit-report.xml)
endif
$(GOTEST) -v -race ./... $(OUTPUT_OPTIONS)

coverage: ## Run the tests of the project and export the coverage
$(GOTEST) -cover -covermode=count -coverprofile=profile.cov ./...
$(GOCMD) tool cover -func profile.cov
ifeq ($(EXPORT_RESULT), true)
GO111MODULE=off go get -u github.com/AlekSi/gocov-xml
GO111MODULE=off go get -u github.com/axw/gocov/gocov
gocov convert profile.cov | gocov-xml > coverage.xml
endif

## Lint:
lint: lint-go lint-dockerfile lint-yaml ## Run all available linters

lint-dockerfile: ## Lint your Dockerfile
# If dockerfile is present we lint it.
ifeq ($(shell test -e ./Dockerfile && echo -n yes),yes)
$(eval CONFIG_OPTION = $(shell [ -e $(shell pwd)/.hadolint.yaml ] && echo "-v $(shell pwd)/.hadolint.yaml:/root/.config/hadolint.yaml" || echo "" ))
$(eval OUTPUT_OPTIONS = $(shell [ "${EXPORT_RESULT}" == "true" ] && echo "--format checkstyle" || echo "" ))
$(eval OUTPUT_FILE = $(shell [ "${EXPORT_RESULT}" == "true" ] && echo "| tee /dev/tty > checkstyle-report.xml" || echo "" ))
docker run --rm -i $(CONFIG_OPTION) hadolint/hadolint hadolint $(OUTPUT_OPTIONS) - < ./Dockerfile $(OUTPUT_FILE)
endif

lint-go: ## Use golintci-lint on your project
$(eval OUTPUT_OPTIONS = $(shell [ "${EXPORT_RESULT}" == "true" ] && echo "--out-format checkstyle ./... | tee /dev/tty > checkstyle-report.xml" || echo "" ))
docker run --rm -v $(shell pwd):/app -w /app golangci/golangci-lint:latest-alpine golangci-lint run --deadline=65s $(OUTPUT_OPTIONS)

lint-yaml: ## Use yamllint on the yaml file of your projects
ifeq ($(EXPORT_RESULT), true)
GO111MODULE=off go get -u github.com/thomaspoignant/yamllint-checkstyle
$(eval OUTPUT_OPTIONS = | tee /dev/tty | yamllint-checkstyle > yamllint-checkstyle.xml)
endif
docker run --rm -it -v $(shell pwd):/data cytopia/yamllint -f parsable $(shell git ls-files '*.yml' '*.yaml') $(OUTPUT_OPTIONS)

## Docker:
docker-build: ## Use the dockerfile to build the container
docker build --rm --tag $(BINARY_NAME) .

docker-release: ## Release the container with tag latest and version
docker tag $(BINARY_NAME) $(DOCKER_REGISTRY)$(BINARY_NAME):latest
docker tag $(BINARY_NAME) $(DOCKER_REGISTRY)$(BINARY_NAME):$(VERSION)
# Push the docker images
docker push $(DOCKER_REGISTRY)$(BINARY_NAME):latest
docker push $(DOCKER_REGISTRY)$(BINARY_NAME):$(VERSION)

## Help:
help: ## Show this help.
@echo ''
@echo 'Usage:'
@echo ' ${YELLOW}make${RESET} ${GREEN}<target>${RESET}'
@echo ''
@echo 'Targets:'
@awk 'BEGIN {FS = ":.*?## "} { \
if (/^[a-zA-Z_-]+:.*?##.*$$/) {printf " ${YELLOW}%-20s${GREEN}%s${RESET}\n", $$1, $$2} \
else if (/^## .*$$/) {printf " ${CYAN}%s${RESET}\n", substr($$1,4)} \
}' $(MAKEFILE_LIST)
38 changes: 38 additions & 0 deletions bindings/go/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@


### Usage
This Go package provides an interface to execute Slangroom using an embedded binary. To use this package, include it in your Go project by importing it:

```go
import "github.com/dyne/slangroom-exec/bindings/go"
```

### Example

```go
package main

import (
"fmt"
"log"
"github.com/dyne/slangroom-exec/bindings/go"
)

func main() {
// Define your contract and inputs
contract := `Rule unknown ignore
Given I fetch the local timestamp in seconds and output into 'timestamp'
Given I have a 'time' named 'timestamp'
Then print the string '😘 Welcome to the Slangroom World 🌈'
Then print the 'timestamp'`

// Execute Slangroom
result, err := slangroom.SlangroomExec("", contract, "", "", "", "")
if err != nil {
log.Fatalf("Execution Failed: %v", err)
}

// Print the execution output
fmt.Println("Execution Output:", result.Output)
}
```
15 changes: 15 additions & 0 deletions bindings/go/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module github.com/dyne/slangroom-exec/bindings/go

go 1.22

require (
github.com/amenzhinsky/go-memexec v0.7.1
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
golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
14 changes: 14 additions & 0 deletions bindings/go/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
github.com/amenzhinsky/go-memexec v0.7.1 h1:DVm4cXzklaNWZoTJgZUi/dlXtelhC7QBtX4luKjl1qk=
github.com/amenzhinsky/go-memexec v0.7.1/go.mod h1:ApTO9/i2bcii7kvIXi74gum+/zYDzkiOXtuBZoYOKVE=
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=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=
golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
Binary file added bindings/go/slangroom-exec
Binary file not shown.
97 changes: 97 additions & 0 deletions bindings/go/slangroom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package slangroom

import (
_ "embed"
"fmt"
"io"
"log"
"strings"

b64 "encoding/base64"

"github.com/amenzhinsky/go-memexec"
)

// Embedding Zenroom binary using go:embed
//
//go:embed slangroom-exec
var slangroomBinary []byte

type SlangResult struct {
Output string
Logs string
}

func SlangroomExec(conf string, contract string, data string, keys string, extra string, context string) (SlangResult, bool) {
exec, err := memexec.New(slangroomBinary)
if err != nil {
log.Fatalf("Failed to load Slangroom executable from memory: %v", err)
}
defer exec.Close()

execCmd := exec.Command("slangroom-exec")

stdout, err := execCmd.StdoutPipe()
if err != nil {
log.Fatalf("Failed to create stdout pipe: %v", err)
}

stderr, err := execCmd.StderrPipe()
if err != nil {
log.Fatalf("Failed to create stderr pipe: %v", err)
}

stdin, err := execCmd.StdinPipe()
if err != nil {
log.Fatalf("Failed to create stdin pipe: %v", err)
}

b64conf := b64.StdEncoding.EncodeToString([]byte(conf))
fmt.Fprintln(stdin, b64conf)

b64contract := b64.StdEncoding.EncodeToString([]byte(contract))
fmt.Fprintln(stdin, b64contract)

b64keys := b64.StdEncoding.EncodeToString([]byte(keys))
fmt.Fprintln(stdin, b64keys)

b64data := b64.StdEncoding.EncodeToString([]byte(data))
fmt.Fprintln(stdin, b64data)

b64extra := b64.StdEncoding.EncodeToString([]byte(extra))
fmt.Fprintln(stdin, b64extra)

b64context := b64.StdEncoding.EncodeToString([]byte(context))
fmt.Fprintln(stdin, b64context)

stdin.Close()

err = execCmd.Start()
if err != nil {
log.Fatalf("Failed to start command: %v", err)
}

stdoutOutput := make(chan string)
stderrOutput := make(chan string)
go captureOutput(stdout, stdoutOutput)
go captureOutput(stderr, stderrOutput)

err = execCmd.Wait()

stdoutStr := <-stdoutOutput
stderrStr := <-stderrOutput

return SlangResult{Output: stdoutStr, Logs: stderrStr}, err == nil
}

func captureOutput(pipe io.ReadCloser, output chan<- string) {
defer close(output)

buf := new(strings.Builder)
_, err := io.Copy(buf, pipe)
if err != nil {
log.Printf("Failed to capture output: %v", err)
return
}
output <- buf.String()
}
76 changes: 76 additions & 0 deletions bindings/go/slangroom_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package slangroom

import (
"encoding/json"
"os"
"testing"

"github.com/stretchr/testify/assert"
)

func TestExecuteSimpleZencode(t *testing.T) {
contract := `Given nothing
Then print the string 'Welcome to slangroom-exec 🥳'`
res, success := SlangroomExec("", contract, "", "", "", "")
assert.JSONEq(t, `{"output":["Welcome_to_slangroom-exec_🥳"]}`, res.Output)
assert.True(t, success, "Expected success but got failure")
}

func TestExecuteSimpleSlangroom(t *testing.T) {
contract := `Rule unknown ignore
Given I fetch the local timestamp in seconds and output into 'timestamp'
Given I have a 'number' named 'timestamp'
Then print the 'timestamp'`
res, success := SlangroomExec("", contract, "", "", "", "")
assert.Contains(t, res.Output, "timestamp")
var result map[string]interface{}
if err := json.Unmarshal([]byte(res.Output), &result); err == nil {
ts, ok := result["timestamp"].(float64)
assert.True(t, ok, "Expected timestamp to be present")
assert.True(t, ts == float64(int(ts)), "Expected timestamp to be a number")
} else {
t.Errorf("Failed to unmarshal output: %v", err)
}
assert.True(t, success, "Expected success but got failure")
}

func TestFailOnBrokenSlangroom(t *testing.T) {
contract := `Gibberish`
res, success := SlangroomExec("", contract, "", "", "", "")
assert.Contains(t, res.Logs, "Invalid Zencode prefix 1: 'Gibberish'")
assert.False(t, success, "Expected failure but got success")
}

func TestFailOnEmptyContract(t *testing.T) {
contract := ``
res, success := SlangroomExec("", contract, "", "", "", "")
assert.Equal(t, "Malformed input: Slangroom contract is empty\n", res.Logs)
assert.False(t, success, "Expected failure but got success")
}

func TestReadDataCorrectly(t *testing.T) {
os.Setenv("FILES_DIR", ".")
filePath := "test/test.txt"
if _, err := os.Stat(filePath); os.IsNotExist(err) {
t.Fatalf("Test file does not exist: %v", err)
}
contract := `Rule unknown ignore
Given I send path 'filename' and read verbatim file content and output into 'content'
Given I have a 'string' named 'filename'
Given I have a 'string' named 'content'
Then print data`
data := `{
"filename": "` + filePath + `"
}`
res, success := SlangroomExec("", contract, data, "", "", "")
assert.Contains(t, res.Output, "Do you know who greets you? 🥒")
assert.True(t, success, "Expected success but got failure")
}

func TestFailOnEmptyOrBrokenContract(t *testing.T) {
contract := ``
conf := `error`
res, success := SlangroomExec(conf, contract, "", "", "", "")
assert.Equal(t, "Malformed input: Slangroom contract is empty\n", res.Logs)
assert.False(t, success, "Expected failure but got success")
}
1 change: 1 addition & 0 deletions bindings/go/test/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Do you know who greets you? 🥒
Loading

0 comments on commit e9ca9a1

Please sign in to comment.