diff --git a/.gitignore b/.gitignore index 9d79b46..c5abb80 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ !/.env.sample !/.cruft.json !/.goreleaser.yaml - +!/test/* !.github/workflows/*.yml !*.go diff --git a/go.mod b/go.mod index 39e56d1..67d5232 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7e9206c --- /dev/null +++ b/go.sum @@ -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= diff --git a/router.go b/router.go new file mode 100644 index 0000000..5366aaa --- /dev/null +++ b/router.go @@ -0,0 +1,137 @@ +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, "

%s

%s
", filepath.Base(filePath), string(content)) + + relativePath, _ := filepath.Rel(baseDir, filePath) + fmt.Fprintf(w, `
+ +
`, relativePath, filepath.Base(filePath)) + + fmt.Fprintln(w, "") + } +} + +// 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, "

Available contract files:

") + + for dir, files := range slangFiles { + fmt.Fprintf(w, "

Directory: %s

") + } + + fmt.Fprintln(w, "") + } +} + +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)) +} diff --git a/router_test.go b/router_test.go new file mode 100644 index 0000000..b1b6dca --- /dev/null +++ b/router_test.go @@ -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(), "

Available contract files:

") + assert.Contains(t, rr.Body.String(), "

Directory: testdir

") + assert.Contains(t, rr.Body.String(), "

Directory: testdir2

") + assert.Contains(t, rr.Body.String(), "testdir/file1.slang") + assert.Contains(t, rr.Body.String(), "testdir/file2.slang") + assert.Contains(t, rr.Body.String(), "testdir2/file3.slang") +} + +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(), ``) +} + +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") +} diff --git a/test/hello .slang b/test/hello .slang new file mode 100644 index 0000000..6d37788 --- /dev/null +++ b/test/hello .slang @@ -0,0 +1,2 @@ +Given nothing +Then print the string 'Hello World!' diff --git a/test/test.slang b/test/test.slang new file mode 100644 index 0000000..6f234a0 --- /dev/null +++ b/test/test.slang @@ -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' diff --git a/test/welcome.slang b/test/welcome.slang new file mode 100644 index 0000000..e9656f2 --- /dev/null +++ b/test/welcome.slang @@ -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'