Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First version #2

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
!/.env.sample
!/.cruft.json
!/.goreleaser.yaml

!/test/*
!.github/workflows/*.yml

!*.go
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@


## 📝 Site docs
## Features

- View the content of `.slang` files.
- Execute slangroom contracts directly via the web interface.

### Usage

-Build the binary with
```bash
go install golang.org/x/pkgsite/cmd/pkgsite@latest && pkgsite
make build
```
-Run the server with
```bash
out/bin/fouter
```

-Open your browser and navigate to:
[http://localhost:3000/slang/](http://localhost:3000/slang/)
17 changes: 15 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
module github.com/dyne/fouter

module github.com/ForkbombEu/fouter
go 1.22

require (
github.com/FilippoTrotter/slangroom-go v0.0.0-20241003095650-aada02d0ab90
github.com/gorilla/mux v1.8.1
github.com/stretchr/testify v1.9.0
)

require (
github.com/amenzhinsky/go-memexec v0.7.1 // indirect
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
)
18 changes: 18 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
github.com/FilippoTrotter/slangroom-go v0.0.0-20241003095650-aada02d0ab90 h1:nuq9jkWM+wY4UfvXHpE0mN/C/W3eIzQp2bds0L9Zao4=
github.com/FilippoTrotter/slangroom-go v0.0.0-20241003095650-aada02d0ab90/go.mod h1:+hMvWLDLPvTCwpx1BFBIoSX/cIlIZDWPvzQ+U1pP3X0=
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/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
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=
132 changes: 132 additions & 0 deletions router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"strings"

slang "github.com/FilippoTrotter/slangroom-go" // Change this to dyne/slangroom-exec once merged
"github.com/gorilla/mux"
)

const baseDir = "."

// Function to find all .slang files recursively in the directory and return a map of directories to their files
func findSlangFiles(dir string) (map[string][]string, error) {
slangFiles := make(map[string][]string)
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(info.Name(), ".slang") {
relativePath, _ := filepath.Rel(baseDir, path) // Get the relative path
dir := filepath.Dir(relativePath)
slangFiles[dir] = append(slangFiles[dir], relativePath)
}
return nil
})
return slangFiles, err
}

// Handler to print the content of the .slang file and provide a button to execute it
func slangFilePageHandler(filePath string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
content, err := os.ReadFile(filePath)
if err != nil {
http.Error(w, "Error reading file", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, "<html><body><h1>%s</h1><pre>%s</pre>", filepath.Base(filePath), string(content))

relativePath, _ := filepath.Rel(baseDir, filePath)
fmt.Fprintf(w, `<form method="POST" action="/slang/execute/%s">
<button type="submit">Execute %s</button>
</form>`, relativePath, filepath.Base(filePath))

fmt.Fprintln(w, "</body></html>")
}
}

// Handler to execute the .slang file content on a POST request
func executeSlangFileHandler(filePath string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}

content, err := os.ReadFile(filePath)
if err != nil {
http.Error(w, "Error reading file", http.StatusInternalServerError)
return
}

result, success := slang.SlangroomExec("", string(content), "", "", "", "")
if !success {
http.Error(w, "Error executing slang file: "+result.Logs, http.StatusInternalServerError)
return
}

output := map[string]interface{}{
"output": result.Output,
}

w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(output); err != nil {
http.Error(w, "Error encoding output to JSON", http.StatusInternalServerError)
}
}
}

// Handler to list all available .slang files, grouped by directory
func listSlangFilesHandler(slangFiles map[string][]string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintln(w, "<html><body><h1>Available contract files:</h1>")

for dir, files := range slangFiles {
fmt.Fprintf(w, "<h2>Directory: %s</h2><ul>", dir)

for _, file := range files {
fileName := filepath.Base(file)
link := fmt.Sprintf("/slang/%s/%s", dir, fileName)
fmt.Fprintf(w, `<li><a href="%s">%s/%s</a></li>`, link, dir, fileName)
}

fmt.Fprintln(w, "</ul>")
}

fmt.Fprintln(w, "</body></html>")
}
}

func main() {
r := mux.NewRouter()

slangFiles, err := findSlangFiles(baseDir)
if err != nil {
log.Fatalf("Error finding .slang files: %v", err)
}

r.HandleFunc("/slang/", listSlangFilesHandler(slangFiles)).Methods("GET")

// For each file, create an API endpoint to show its content and add an execution button
for _, files := range slangFiles {
for _, file := range files {
// Create a handler to show file content and an execute button
r.HandleFunc(fmt.Sprintf("/slang/%s", file), slangFilePageHandler(file)).Methods("GET")

// Create a handler to execute the file
r.HandleFunc(fmt.Sprintf("/slang/execute/%s", file), executeSlangFileHandler(file)).Methods("POST")
}
}

fmt.Println("Starting server on :3000")
fmt.Println("Access the contract files at: http://localhost:3000/slang/")
log.Fatal(http.ListenAndServe(":3000", r))
}
117 changes: 117 additions & 0 deletions router_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package main

import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"

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

func TestListSlangFiles(t *testing.T) {
// Sample slang files data for testing
slangFiles := map[string][]string{
"testdir": {"file1.slang", "file2.slang"},
"testdir2": {"file3.slang"},
}

// Create a request to pass to the handler
req, err := http.NewRequest("GET", "/slang/", nil)
if err != nil {
t.Fatal(err)
}

// Create a ResponseRecorder to record the response
rr := httptest.NewRecorder()

// Create the handler
handler := listSlangFilesHandler(slangFiles)

// Call the handler
handler.ServeHTTP(rr, req)

// Check the response status code
assert.Equal(t, http.StatusOK, rr.Code)

// Check that the response contains the expected content
assert.Contains(t, rr.Body.String(), "<h1>Available contract files:</h1>")
assert.Contains(t, rr.Body.String(), "<h2>Directory: testdir</h2>")
assert.Contains(t, rr.Body.String(), "<h2>Directory: testdir2</h2>")
assert.Contains(t, rr.Body.String(), "<a href=\"/slang/testdir/file1.slang\">testdir/file1.slang</a>")
assert.Contains(t, rr.Body.String(), "<a href=\"/slang/testdir/file2.slang\">testdir/file2.slang</a>")
assert.Contains(t, rr.Body.String(), "<a href=\"/slang/testdir2/file3.slang\">testdir2/file3.slang</a>")
}

func TestSlangFilePage(t *testing.T) {
// Set up a test slang file
testFileName := "testfile.slang"
testFileContent := "Given nothing\nThen print the string 'Test Successful'"
_ = os.WriteFile(testFileName, []byte(testFileContent), 0644)
defer os.Remove(testFileName) // Cleanup after test

req, err := http.NewRequest("GET", "/slang/"+testFileName, nil)
if err != nil {
t.Fatal(err)
}

rr := httptest.NewRecorder()
handler := slangFilePageHandler(testFileName)

handler.ServeHTTP(rr, req)

// Check the response code
assert.Equal(t, http.StatusOK, rr.Code)

// Check if the file content is in the response
assert.Contains(t, rr.Body.String(), testFileContent)

// Check for the execution button in the response
assert.Contains(t, rr.Body.String(), `<button type="submit">Execute testfile.slang</button>`)
}

func TestExecuteSlangFile(t *testing.T) {
// Set up a test slang file
testFileName := "testexecfile.slang"
testFileContent := `Given nothing
Then print the string 'Execution Successful'`
_ = os.WriteFile(testFileName, []byte(testFileContent), 0644)
defer os.Remove(testFileName) // Cleanup after test

// Simulate a POST request to execute the slang file
req, err := http.NewRequest("POST", "/slang/execute/"+testFileName, nil)
if err != nil {
t.Fatal(err)
}

rr := httptest.NewRecorder()
handler := executeSlangFileHandler(testFileName)

handler.ServeHTTP(rr, req)

// Check the response code
assert.Equal(t, http.StatusOK, rr.Code)

// Check the output of the execution (adapt based on your expected output)
var result map[string]interface{}
err = json.Unmarshal(rr.Body.Bytes(), &result)
assert.NoError(t, err, "Failed to unmarshal output")
assert.Contains(t, result["output"], "Execution_Successful")
}

func TestInvalidRequestMethod(t *testing.T) {
req, err := http.NewRequest("GET", "/slang/execute/testfile.slang", nil)
if err != nil {
t.Fatal(err)
}

rr := httptest.NewRecorder()
handler := executeSlangFileHandler("testfile.slang") // Assuming a dummy file for handler

handler.ServeHTTP(rr, req)

// Check the response code for invalid method
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid request method")
}
2 changes: 2 additions & 0 deletions test/hello .slang
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Given nothing
Then print the string 'Hello World!'
5 changes: 5 additions & 0 deletions test/test.slang
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
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'
4 changes: 4 additions & 0 deletions test/welcome.slang
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
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'