From 9479bb9abae45dfaeda7af03367e630b32621f1d Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Wed, 6 Mar 2024 22:18:25 +0100 Subject: [PATCH 01/14] Added link to server on startup --- misc/Misc.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/misc/Misc.go b/misc/Misc.go index ba77f6c..7e6378c 100644 --- a/misc/Misc.go +++ b/misc/Misc.go @@ -3,6 +3,7 @@ package misc import ( "fmt" "github.com/rs/zerolog/log" + "net" "net/http" "strconv" ) @@ -13,14 +14,31 @@ func WelcomeMessage() { fmt.Println(text) } +func GetLocalIP() string { + addrs, err := net.InterfaceAddrs() + if err != nil { + return "" + } + for _, address := range addrs { + // check the address type and if it is not a loopback the display it + if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { + if ipnet.IP.To4() != nil { + return ipnet.IP.String() + } + } + } + return "" +} + // Startup a http server on a port func StartHttp(port int) { - log.Info().Msg("Starting HTTP server...") + ip := GetLocalIP() + url := fmt.Sprintf("http://%s:%d", ip, port) + log.Info().Msgf("Server is running at: %s", url) err := http.ListenAndServe(":"+strconv.Itoa(port), nil) if err != nil { log.Fatal().Err(err).Msg("Fatal Error with http server") } - } // Capitalize a string From b33793f96fe8944d403c12880f2f58f073cdd1e1 Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Thu, 7 Mar 2024 22:23:56 +0100 Subject: [PATCH 02/14] Redundant logging removed --- components/model/TypeSpecGeneration.go | 1 - configuration/Config.go | 1 - 2 files changed, 2 deletions(-) diff --git a/components/model/TypeSpecGeneration.go b/components/model/TypeSpecGeneration.go index 6f372aa..744b34f 100644 --- a/components/model/TypeSpecGeneration.go +++ b/components/model/TypeSpecGeneration.go @@ -32,7 +32,6 @@ func matchesSpec(Y jsonmap.Map, T reflect.Type) bool { // Compare the types of the field in the struct and in the JSON map if fieldT != reflect.TypeOf(fieldValue) { if fieldT == reflect.TypeOf(int(0)) && reflect.TypeOf(fieldValue) == reflect.TypeOf(float64(0)) { - log.Warn().Msg("Adapted type to int from float64") } else { log.Error().Msgf("Wrong Type in field '%s' in JSON request. Got type '%s' expected type '%s'", fieldName, fieldT, reflect.TypeOf(fieldValue)) return false diff --git a/configuration/Config.go b/configuration/Config.go index 8b8715e..de765d1 100644 --- a/configuration/Config.go +++ b/configuration/Config.go @@ -69,7 +69,6 @@ func (c configuration) adapt() (*Configuration, error) { var databaseClosure func() if c.Database.Path == "" || c.Database.InitQuery == "" { - log.Warn().Msg("Missing Database in main.yml : Models are disabled") // Set all the models to nil, effectively disabling models for i := 0; i < len(c.Controllers); i++ { newController, err := c.Controllers[i].adapt(nil) From da2686ee066f2fb871f33e0955b6224b2cfabfbb Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Fri, 8 Mar 2024 00:08:59 +0100 Subject: [PATCH 03/14] added Cache header --- components/controller/controller.go | 19 ++++++++++++------- components/controller/controller_test.go | 12 ++++++------ components/model/Model.go | 4 +++- configuration/ConfigPrimitiveTypes.go | 3 ++- configuration/adapter_test.go | 4 ++-- docs/external/component's.md | 6 ++++-- docs/internal/README.md | 2 +- docs/internal/project-structure.md | 12 ++++++------ docs/internal/{tools.md => scripts.md} | 4 ++-- 9 files changed, 38 insertions(+), 28 deletions(-) rename docs/internal/{tools.md => scripts.md} (87%) diff --git a/components/controller/controller.go b/components/controller/controller.go index a832f88..36d90b4 100644 --- a/components/controller/controller.go +++ b/components/controller/controller.go @@ -22,12 +22,13 @@ type Controller struct { Model *model.Model Fallback []byte cors string + Cache string http.Handler } /* Constructor for the controller, outside of package used like this 'Controller.Create(x,y)' */ -func Create(name string, datamodel *model.Model, fallback []byte, cors string) Controller { - return Controller{Name: name, Model: datamodel, Fallback: fallback, cors: cors} +func Create(name string, datamodel *model.Model, fallback []byte, cors string, cache string) Controller { + return Controller{Name: name, Model: datamodel, Fallback: fallback, cors: cors, Cache: cache} } func (c Controller) handleNoModelRequest(w http.ResponseWriter) { @@ -39,17 +40,20 @@ func (c Controller) handleNoModelRequest(w http.ResponseWriter) { return } } -func (c Controller) handleCors(w http.ResponseWriter) { +func (c Controller) handleHeaders(w http.ResponseWriter) { if c.cors != "" { w.Header().Set("Access-Control-Allow-Origin", c.cors) } + if c.Cache != "" { + w.Header().Set("Cache-Control", c.Cache) + } } /* logic is the function to fulfill the http.Handler interface. */ func (c Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) { - //Enable cors - c.handleCors(w) + //set Headers + c.handleHeaders(w) if c.Model == nil { c.handleNoModelRequest(w) } else { @@ -61,7 +65,7 @@ func (c Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - + log.Trace().Msg("Building Query in : " + c.Name) query, err := c.Model.Querybuilder(body) if err != nil { if err.Error() == "JSON request does not match spec" { @@ -74,7 +78,7 @@ func (c Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } } - + log.Trace().Msg("Running Query in : " + c.Name) // Make the db query result, err := c.Model.Query(query) if err != nil { @@ -120,6 +124,7 @@ func (c Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) // Send the JSON response + log.Trace().Msg("Sending response in : " + c.Name) _, err = w.Write(resp) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) diff --git a/components/controller/controller_test.go b/components/controller/controller_test.go index b221afe..f84f81c 100644 --- a/components/controller/controller_test.go +++ b/components/controller/controller_test.go @@ -13,7 +13,7 @@ import ( /*Testing for a no model Controller using the fallback string*/ func TestController_ServeHTTP_BasicString(t *testing.T) { x, _ := json.Marshal("Hello World") - c := Create("basicTest", nil, x, "") + c := Create("basicTest", nil, x, "", "") req := httptest.NewRequest("GET", "http://google.com", nil) w := httptest.NewRecorder() c.ServeHTTP(w, req) @@ -38,7 +38,7 @@ func TestController_ServeHTTP_BasicString(t *testing.T) { /*Testing for a no model Controller using the fallback string*/ func TestController_ServeHTTP_BasicInt(t *testing.T) { x, _ := json.Marshal(69) - c := Create("basicTest", nil, x, "") + c := Create("basicTest", nil, x, "", "") req := httptest.NewRequest("GET", "http://google.com", nil) w := httptest.NewRecorder() c.ServeHTTP(w, req) @@ -76,7 +76,7 @@ func TestController_ServeHTTP_Struct(t *testing.T) { } // Create a request using the input data - c := Create("basicTest", nil, requestData, "") + c := Create("basicTest", nil, requestData, "", "") req := httptest.NewRequest("GET", "http://google.com", nil) w := httptest.NewRecorder() @@ -108,7 +108,7 @@ func TestController_Create(t *testing.T) { expectedFallback, _ := json.Marshal(69) // Call the Create function - c := Create(expectedName, nil, expectedFallback, "") + c := Create(expectedName, nil, expectedFallback, "", "") // Check if the fields of the created controller match the expected values if c.Name != expectedName { @@ -129,8 +129,8 @@ func TestSetupControllers(t *testing.T) { Fallback2, _ := json.Marshal("Hello World") // Create controllers and add them to Services map - Services["/get_int"] = Create("int_controller", nil, Fallback1, "") - Services["/get_str"] = Create("str_controller", nil, Fallback2, "") + Services["/get_int"] = Create("int_controller", nil, Fallback1, "", "") + Services["/get_str"] = Create("str_controller", nil, Fallback2, "", "") // Setup controllers SetupControllers(Services) diff --git a/components/model/Model.go b/components/model/Model.go index 1a973e5..9c073e0 100644 --- a/components/model/Model.go +++ b/components/model/Model.go @@ -37,13 +37,15 @@ func (m Model) Querybuilder(x []byte) (string, error) { err := json.Unmarshal(x, jsonRequest) if err != nil { - return "", errors.New("failed to decode JSON data: " + err.Error()) + log.Debug().Msg("Failed to decode :" + string(x)) + return "", errors.New("Failed to decode JSON data: " + err.Error()) } //Basic type caching var GeneratedType reflect.Type if m.generatedTypeCache == nil { GeneratedType = GenerateStructFromJsonMap(*m.json) m.generatedTypeCache = &GeneratedType + log.Trace().Msg("Caching Type for model : " + m.Name) } else { GeneratedType = *m.generatedTypeCache } diff --git a/configuration/ConfigPrimitiveTypes.go b/configuration/ConfigPrimitiveTypes.go index befcdda..8231da1 100644 --- a/configuration/ConfigPrimitiveTypes.go +++ b/configuration/ConfigPrimitiveTypes.go @@ -35,6 +35,7 @@ type Controller struct { Name string `yaml:"name"` Model string `yaml:"model"` Cors string `yaml:"cors"` + Cache string `yaml:"cache"` } func (c Controller) adapt(model *model2.Model) (controller.Controller, error) { @@ -42,7 +43,7 @@ func (c Controller) adapt(model *model2.Model) (controller.Controller, error) { if err != nil { return controller.Controller{}, errors.New(fmt.Sprintf("Json error in Controller : %s", c.Name)) } - return controller.Create(c.Name, model, JSON, c.Cors), nil + return controller.Create(c.Name, model, JSON, c.Cors, c.Cache), nil } // Struct representing a single field of a json spec diff --git a/configuration/adapter_test.go b/configuration/adapter_test.go index 2c5e2a2..a926341 100644 --- a/configuration/adapter_test.go +++ b/configuration/adapter_test.go @@ -46,7 +46,7 @@ func TestModelAdaptModel(t *testing.T) { func TestModelAdaptController(t *testing.T) { - sample := Controller{Name: "name", Fallback: "ok", Model: "", Cors: "*"} + sample := Controller{Name: "name", Fallback: "ok", Model: "", Cors: "*", Cache: ""} x, err := sample.adapt(nil) // Check if error is nil if err != nil { @@ -54,7 +54,7 @@ func TestModelAdaptController(t *testing.T) { } // Create an expected controller - expected := controller.Create("name", nil, []byte(`"ok"`), "*") + expected := controller.Create("name", nil, []byte(`"ok"`), "*", "") // Check if the adapted model is equal to the expected model using reflection if !reflect.DeepEqual(x, expected) { diff --git a/docs/external/component's.md b/docs/external/component's.md index d384885..77de17e 100644 --- a/docs/external/component's.md +++ b/docs/external/component's.md @@ -32,7 +32,8 @@ $controller: - name: name1 fallback: example model: model1 - cors: "*" + cors: "*" + cache: "max-age=3600, public" # name -> This is the name of the controller. YOou use this to attach it to other component's @@ -41,7 +42,8 @@ $controller: # model -> Attaches data handling to a controller, read up on them at the 'Model' section. - # cors -> Sets a cors value to input string, without setting it, nothing gets set + # cors -> Sets a cors header value + # cache -> Sets a cache-control header value ``` ### Database ```yaml diff --git a/docs/internal/README.md b/docs/internal/README.md index 551db61..94c14cd 100644 --- a/docs/internal/README.md +++ b/docs/internal/README.md @@ -12,4 +12,4 @@ This folder contains all the documentation for how Scaffold works. Read through ## Content's: 1. [Project Structure](project-structure.md) -2. [Project Tools](tools.md) \ No newline at end of file +2. [Project Scripts](scripts.md) \ No newline at end of file diff --git a/docs/internal/project-structure.md b/docs/internal/project-structure.md index 375e3a5..fab2dbd 100644 --- a/docs/internal/project-structure.md +++ b/docs/internal/project-structure.md @@ -4,10 +4,10 @@ ## Root | Path | Usage | |------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| -| /cmd | This package contains functions to be called from the CLI | -| /components | This directory contains the component packages | +| /cmd | This package contains functions to be called from the CLI | +| /components | This directory contains the component packages | | /configuration | This package should contain anything related to setting up the application from the config file | -| /docs | This directory contains the documentation for the project. Internal is for developers to reference when building. External for users. | -| /e2e | This directory contains end-to-end tests. E2E tests are written in Ruby | -| /misc | This package contains utility functions like Capitalize etc. | -| /tools | This directory contains developer's tools | +| /docs | This directory contains the documentation for the project. Internal is for developers to reference when building. External for users. | +| /e2e | This directory contains end-to-end tests. E2E tests are written in Ruby | +| /misc | This package contains utility functions like Capitalize etc. | +| /scripts | This directory contains developer tools and install scripts | diff --git a/docs/internal/tools.md b/docs/internal/scripts.md similarity index 87% rename from docs/internal/tools.md rename to docs/internal/scripts.md index 56e7cb8..8db8d24 100644 --- a/docs/internal/tools.md +++ b/docs/internal/scripts.md @@ -1,4 +1,4 @@ -# Tooling for Scaffold +# Scaffold scripts --- ## Build tool (Ruby) @@ -6,7 +6,7 @@ ### How to run To run the build tool, run this command from the project root: ```ruby - ruby ./tools/build.rb + ruby ./scripts/build.rb ``` ### What it does The tool will build two folders, one with a linux build and the other with a window. From df9f7fe9f24cfd1d3bbe1658ff5995a8d7fea423 Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Fri, 8 Mar 2024 00:09:27 +0100 Subject: [PATCH 04/14] Changed from tools to scripts --- {tools => scripts}/build.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {tools => scripts}/build.rb (100%) diff --git a/tools/build.rb b/scripts/build.rb similarity index 100% rename from tools/build.rb rename to scripts/build.rb From 2ecde7b1a8352a741ecec2631109b3fc9e2bfce4 Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Fri, 8 Mar 2024 00:33:08 +0100 Subject: [PATCH 05/14] Added E2E testcase for cache-control --- e2e/test_configs/no_database/main.yml | 1 + e2e/test_configs/no_database/test.rb | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/e2e/test_configs/no_database/main.yml b/e2e/test_configs/no_database/main.yml index ded104a..e4df1b9 100644 --- a/e2e/test_configs/no_database/main.yml +++ b/e2e/test_configs/no_database/main.yml @@ -6,6 +6,7 @@ $controller: name: status_controller - fallback: 69 name: int_controller + cache: "max-age=604800" server: port: 8080 $service: diff --git a/e2e/test_configs/no_database/test.rb b/e2e/test_configs/no_database/test.rb index d8f3ae1..e2e9a22 100644 --- a/e2e/test_configs/no_database/test.rb +++ b/e2e/test_configs/no_database/test.rb @@ -57,12 +57,35 @@ # Convert response to JSON json_response = JSON.parse(response.body) + + + if json_response != 69 puts "Wrong json response received in Test 3: #{json_response}" system("taskkill /f /im main.exe > NUL 2>&1") exit 1 end +cache_found = false + +response.each_capitalized do |key, value| + if key == 'Cache-Control' + cache_found = true + if value != "max-age=3600, public" + puts "Wrong Cache received: #{value}" + system("taskkill /f /im main.exe > NUL 2>&1") + exit 1 + end + end +end + +unless cache_found + puts "Cache header not found" + system("taskkill /f /im main.exe > NUL 2>&1") + exit 1 +end + + # Kill the process system("taskkill /f /im main.exe > NUL 2>&1") exit 0 \ No newline at end of file From 81c97ddef7f931fee0a64b0ab7d2175644244a14 Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Fri, 8 Mar 2024 02:28:22 +0100 Subject: [PATCH 06/14] Added file based routing for static field in server component --- Scaffold.go | 2 +- components/controller/controller.go | 3 ++- components/model/Model.go | 1 - docs/external/component's.md | 2 +- misc/Misc.go | 23 +++++++++++++++++++++-- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/Scaffold.go b/Scaffold.go index c0a0662..99c5a09 100644 --- a/Scaffold.go +++ b/Scaffold.go @@ -86,7 +86,7 @@ func main() { elapsed := end.Sub(start) log.Info().Msgf("Project built in : %s", elapsed) - misc.StartHttp(conf.Server.Port) + misc.StartHttp(conf.Server.Port, conf.Server.Static) } if flag.NArg() == 0 { // Print help message diff --git a/components/controller/controller.go b/components/controller/controller.go index 36d90b4..7c222c5 100644 --- a/components/controller/controller.go +++ b/components/controller/controller.go @@ -66,6 +66,7 @@ func (c Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } log.Trace().Msg("Building Query in : " + c.Name) + // make the db query query, err := c.Model.Querybuilder(body) if err != nil { if err.Error() == "JSON request does not match spec" { @@ -79,7 +80,7 @@ func (c Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } log.Trace().Msg("Running Query in : " + c.Name) - // Make the db query + // Queries the database result, err := c.Model.Query(query) if err != nil { log.Err(err).Msg("Something went wrong with querying database") diff --git a/components/model/Model.go b/components/model/Model.go index 9c073e0..1c4646d 100644 --- a/components/model/Model.go +++ b/components/model/Model.go @@ -77,7 +77,6 @@ func (m Model) Querybuilder(x []byte) (string, error) { } -// Queries the database func (m Model) Query(query string) (*sql.Rows, error) { rows, err := m.db.Query(query) if err != nil { diff --git a/docs/external/component's.md b/docs/external/component's.md index 77de17e..f87bf6e 100644 --- a/docs/external/component's.md +++ b/docs/external/component's.md @@ -17,7 +17,7 @@ server: route: /Example # port -> Set's the server's port to the int value. - # static -> display's the static content of the input server @ path '/' + # static -> display's the static content of the input server @ path '/'. Uses file based routing # target-log -> Set's the target file for logging, if left empty it only prints to stdout # $service -> Connects an endpoint to a Scaffold Controller # controller -> Set the controller for the specific service. These can be reused. Use the controller's name. diff --git a/misc/Misc.go b/misc/Misc.go index 7e6378c..e20b14a 100644 --- a/misc/Misc.go +++ b/misc/Misc.go @@ -6,6 +6,7 @@ import ( "net" "net/http" "strconv" + "strings" ) // A simple message to display on startup @@ -31,11 +32,29 @@ func GetLocalIP() string { } // Startup a http server on a port -func StartHttp(port int) { +func StartHttp(port int, static string) { + mux := http.NewServeMux() + + // Handler to serve HTML files without .html extension + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + if path == "/" { + path = "/index.html" // Serve root + } else { + if strings.HasSuffix(path, "/") { + path = strings.TrimSuffix(path, "/") + } + if !strings.HasSuffix(path, ".html") { + path += ".html" // Append .html extension if not present + } + } + http.ServeFile(w, r, static+path) + }) ip := GetLocalIP() url := fmt.Sprintf("http://%s:%d", ip, port) log.Info().Msgf("Server is running at: %s", url) - err := http.ListenAndServe(":"+strconv.Itoa(port), nil) + + err := http.ListenAndServe(":"+strconv.Itoa(port), mux) if err != nil { log.Fatal().Err(err).Msg("Fatal Error with http server") } From 278f2b7b76f32744b1b7452951cf80f87ee5caba Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Fri, 8 Mar 2024 04:47:15 +0100 Subject: [PATCH 07/14] Added http verbs + Removed breaking change with no file extension --- Scaffold.go | 2 +- components/controller/controller.go | 16 ++++++++------ components/controller/controller_test.go | 12 +++++------ configuration/ConfigPrimitiveTypes.go | 12 ++++++++++- configuration/adapter_test.go | 2 +- docs/external/component's.md | 4 +++- e2e/test_configs/no_database/main.yml | 2 +- misc/Misc.go | 27 ++++-------------------- 8 files changed, 37 insertions(+), 40 deletions(-) diff --git a/Scaffold.go b/Scaffold.go index 99c5a09..c0a0662 100644 --- a/Scaffold.go +++ b/Scaffold.go @@ -86,7 +86,7 @@ func main() { elapsed := end.Sub(start) log.Info().Msgf("Project built in : %s", elapsed) - misc.StartHttp(conf.Server.Port, conf.Server.Static) + misc.StartHttp(conf.Server.Port) } if flag.NArg() == 0 { // Print help message diff --git a/components/controller/controller.go b/components/controller/controller.go index 7c222c5..74a7da6 100644 --- a/components/controller/controller.go +++ b/components/controller/controller.go @@ -22,13 +22,14 @@ type Controller struct { Model *model.Model Fallback []byte cors string - Cache string + cache string + verb string http.Handler } /* Constructor for the controller, outside of package used like this 'Controller.Create(x,y)' */ -func Create(name string, datamodel *model.Model, fallback []byte, cors string, cache string) Controller { - return Controller{Name: name, Model: datamodel, Fallback: fallback, cors: cors, Cache: cache} +func Create(name string, datamodel *model.Model, fallback []byte, cors string, cache string, verb string) Controller { + return Controller{Name: name, Model: datamodel, Fallback: fallback, cors: cors, cache: cache, verb: verb} } func (c Controller) handleNoModelRequest(w http.ResponseWriter) { @@ -44,14 +45,18 @@ func (c Controller) handleHeaders(w http.ResponseWriter) { if c.cors != "" { w.Header().Set("Access-Control-Allow-Origin", c.cors) } - if c.Cache != "" { - w.Header().Set("Cache-Control", c.Cache) + if c.cache != "" { + w.Header().Set("Cache-Control", c.cache) } } /* logic is the function to fulfill the http.Handler interface. */ func (c Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if c.verb != "" && c.verb != r.Method { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } //set Headers c.handleHeaders(w) if c.Model == nil { @@ -145,7 +150,6 @@ func SetupControllers(services map[string]Controller) { if route == "" { log.Fatal().Err(errors.New("Missing route")).Msg("Something went wrong with setting up Controllers") } - http.Handle(route, handler) if handler.Name == "" { wrn = append(wrn, fmt.Sprintf("Empty controller for Route: '%s'", route)) diff --git a/components/controller/controller_test.go b/components/controller/controller_test.go index f84f81c..abd4916 100644 --- a/components/controller/controller_test.go +++ b/components/controller/controller_test.go @@ -13,7 +13,7 @@ import ( /*Testing for a no model Controller using the fallback string*/ func TestController_ServeHTTP_BasicString(t *testing.T) { x, _ := json.Marshal("Hello World") - c := Create("basicTest", nil, x, "", "") + c := Create("basicTest", nil, x, "", "", "") req := httptest.NewRequest("GET", "http://google.com", nil) w := httptest.NewRecorder() c.ServeHTTP(w, req) @@ -38,7 +38,7 @@ func TestController_ServeHTTP_BasicString(t *testing.T) { /*Testing for a no model Controller using the fallback string*/ func TestController_ServeHTTP_BasicInt(t *testing.T) { x, _ := json.Marshal(69) - c := Create("basicTest", nil, x, "", "") + c := Create("basicTest", nil, x, "", "", "") req := httptest.NewRequest("GET", "http://google.com", nil) w := httptest.NewRecorder() c.ServeHTTP(w, req) @@ -76,7 +76,7 @@ func TestController_ServeHTTP_Struct(t *testing.T) { } // Create a request using the input data - c := Create("basicTest", nil, requestData, "", "") + c := Create("basicTest", nil, requestData, "", "", "") req := httptest.NewRequest("GET", "http://google.com", nil) w := httptest.NewRecorder() @@ -108,7 +108,7 @@ func TestController_Create(t *testing.T) { expectedFallback, _ := json.Marshal(69) // Call the Create function - c := Create(expectedName, nil, expectedFallback, "", "") + c := Create(expectedName, nil, expectedFallback, "", "", "") // Check if the fields of the created controller match the expected values if c.Name != expectedName { @@ -129,8 +129,8 @@ func TestSetupControllers(t *testing.T) { Fallback2, _ := json.Marshal("Hello World") // Create controllers and add them to Services map - Services["/get_int"] = Create("int_controller", nil, Fallback1, "", "") - Services["/get_str"] = Create("str_controller", nil, Fallback2, "", "") + Services["/get_int"] = Create("int_controller", nil, Fallback1, "", "", "") + Services["/get_str"] = Create("str_controller", nil, Fallback2, "", "", "") // Setup controllers SetupControllers(Services) diff --git a/configuration/ConfigPrimitiveTypes.go b/configuration/ConfigPrimitiveTypes.go index 8231da1..1d8872f 100644 --- a/configuration/ConfigPrimitiveTypes.go +++ b/configuration/ConfigPrimitiveTypes.go @@ -8,6 +8,7 @@ import ( "github.com/metalim/jsonmap" "service/components/controller" model2 "service/components/model" + "strings" ) // ConfigPrimitiveTypes should include all the non-critical structs and their methods @@ -36,6 +37,7 @@ type Controller struct { Model string `yaml:"model"` Cors string `yaml:"cors"` Cache string `yaml:"cache"` + Verb string `yaml:"verb"` } func (c Controller) adapt(model *model2.Model) (controller.Controller, error) { @@ -43,7 +45,15 @@ func (c Controller) adapt(model *model2.Model) (controller.Controller, error) { if err != nil { return controller.Controller{}, errors.New(fmt.Sprintf("Json error in Controller : %s", c.Name)) } - return controller.Create(c.Name, model, JSON, c.Cors, c.Cache), nil + verb := strings.ToUpper(c.Verb) + switch verb { + case "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", "": + default: + err := errors.New("Unrecognized HTTP method") + fmt.Println(err) + return controller.Controller{}, err + } + return controller.Create(c.Name, model, JSON, c.Cors, c.Cache, verb), nil } // Struct representing a single field of a json spec diff --git a/configuration/adapter_test.go b/configuration/adapter_test.go index a926341..2ee0e78 100644 --- a/configuration/adapter_test.go +++ b/configuration/adapter_test.go @@ -54,7 +54,7 @@ func TestModelAdaptController(t *testing.T) { } // Create an expected controller - expected := controller.Create("name", nil, []byte(`"ok"`), "*", "") + expected := controller.Create("name", nil, []byte(`"ok"`), "*", "", "") // Check if the adapted model is equal to the expected model using reflection if !reflect.DeepEqual(x, expected) { diff --git a/docs/external/component's.md b/docs/external/component's.md index f87bf6e..5a6ded7 100644 --- a/docs/external/component's.md +++ b/docs/external/component's.md @@ -31,7 +31,8 @@ Controllers are the point of entry for your application's users. They attach bas $controller: - name: name1 fallback: example - model: model1 + model: model1 + verb: GET cors: "*" cache: "max-age=3600, public" @@ -42,6 +43,7 @@ $controller: # model -> Attaches data handling to a controller, read up on them at the 'Model' section. + # verb -> Only allows requests pf this request type / request method # cors -> Sets a cors header value # cache -> Sets a cache-control header value ``` diff --git a/e2e/test_configs/no_database/main.yml b/e2e/test_configs/no_database/main.yml index e4df1b9..3d194f6 100644 --- a/e2e/test_configs/no_database/main.yml +++ b/e2e/test_configs/no_database/main.yml @@ -6,7 +6,7 @@ $controller: name: status_controller - fallback: 69 name: int_controller - cache: "max-age=604800" + cache: "max-age=3600, public" server: port: 8080 $service: diff --git a/misc/Misc.go b/misc/Misc.go index e20b14a..942e83a 100644 --- a/misc/Misc.go +++ b/misc/Misc.go @@ -6,7 +6,6 @@ import ( "net" "net/http" "strconv" - "strings" ) // A simple message to display on startup @@ -30,34 +29,16 @@ func GetLocalIP() string { } return "" } - -// Startup a http server on a port -func StartHttp(port int, static string) { - mux := http.NewServeMux() - - // Handler to serve HTML files without .html extension - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - if path == "/" { - path = "/index.html" // Serve root - } else { - if strings.HasSuffix(path, "/") { - path = strings.TrimSuffix(path, "/") - } - if !strings.HasSuffix(path, ".html") { - path += ".html" // Append .html extension if not present - } - } - http.ServeFile(w, r, static+path) - }) +func StartHttp(port int) { + log.Info().Msg("Starting HTTP server...") ip := GetLocalIP() url := fmt.Sprintf("http://%s:%d", ip, port) log.Info().Msgf("Server is running at: %s", url) - - err := http.ListenAndServe(":"+strconv.Itoa(port), mux) + err := http.ListenAndServe(":"+strconv.Itoa(port), nil) if err != nil { log.Fatal().Err(err).Msg("Fatal Error with http server") } + } // Capitalize a string From 3cdcca4f272bb6ad17330d2bda397462eee18ac9 Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Fri, 8 Mar 2024 05:00:41 +0100 Subject: [PATCH 08/14] Documented the cli and how to use it --- docs/external/README.md | 5 +++-- docs/external/cli.md | 31 +++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 docs/external/cli.md diff --git a/docs/external/README.md b/docs/external/README.md index 566d7a4..1a23b45 100644 --- a/docs/external/README.md +++ b/docs/external/README.md @@ -5,8 +5,9 @@ This folder contains all the doc's for the api and how to configure your Scaffol application. ## Content's: -1. [Components](component's.md) -2. [Good practices](good-practices.md) +1. [Scaffold Cli](./cli.md) +2. [Components](component's.md) +3. [Good practices](good-practices.md) ## Flow Chart for Scaffold's API process Scaffold has a simple yet effective process. diff --git a/docs/external/cli.md b/docs/external/cli.md new file mode 100644 index 0000000..7873f99 --- /dev/null +++ b/docs/external/cli.md @@ -0,0 +1,31 @@ +# Scaffold CLI + +The cli is the basic interface for running your Scaffold app. This page explains all the commands you can use. + +## version +Displays the version of Scaffold you're using. +```batch +Scaffold version +``` + +## run +Start's your scaffold project, it requires a main.yml file to start. +```batch +Scaffold run [project name] +``` +You can use this to run the current directory. +``` +Scaffold run . +``` + +## init +Create a new Scaffold project. +```batch +Scaffold init [project name] +``` + +## auto-doc (experimental feature) +Automatically generates documentation in a auto-doc.md file,this gives a very rough documenation of the endpoint's. +```batch +Scaffold auto-doc [project name] +``` From 817bbfa05c439c270b9bf0ed67ac72f985decd91 Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Fri, 8 Mar 2024 05:23:36 +0100 Subject: [PATCH 09/14] Added GetJsonTemplate --- components/model/Model.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/components/model/Model.go b/components/model/Model.go index 1c4646d..5252f5c 100644 --- a/components/model/Model.go +++ b/components/model/Model.go @@ -25,6 +25,9 @@ func Create(name string, db *sql.DB, template string, JSON *jsonmap.Map) Model { func (m Model) GetQuery() string { return m.queryTemplate } +func (m Model) GetJsonTemplate() *jsonmap.Map { + return m.json +} // Fills out the query queryTemplate with data from the json func (m Model) Querybuilder(x []byte) (string, error) { From 2612654f1d3baee577a38a724792729ad334eb73 Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Fri, 8 Mar 2024 05:23:55 +0100 Subject: [PATCH 10/14] Added JSON spec generation for auto-doc --- cmd/cmd.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cmd/cmd.go b/cmd/cmd.go index 58a3924..fba755f 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -106,6 +106,16 @@ func GenerateDoc(path string) { docString.WriteString("This route returns:\n```JSON\n" + string(value.Fallback) + "\n```\n") } else { docString.WriteString("This route runs the query:\n ```SQL\n" + value.Model.GetQuery() + "\n```\n") + if value.Model.GetJsonTemplate().Len() != 0 { + jsonT := value.Model.GetJsonTemplate() + // Generate JSON example + docString.WriteString("JSON Specification:\n```json\n{\n") + for _, Name := range jsonT.Keys() { + T, _ := jsonT.Get(Name) + docString.WriteString(fmt.Sprintf(" %s : %s\n", Name, T)) + } + docString.WriteString("}\n```\n") + } docString.WriteString("and fallsback to:\n ```JSON\n" + string(value.Fallback) + "\n```\n") } } From 27087b55acec30862764a9b5526904382efcd316 Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Fri, 8 Mar 2024 06:18:09 +0100 Subject: [PATCH 11/14] improved test for http verb --- configuration/adapter_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration/adapter_test.go b/configuration/adapter_test.go index 2ee0e78..a3a8965 100644 --- a/configuration/adapter_test.go +++ b/configuration/adapter_test.go @@ -46,7 +46,7 @@ func TestModelAdaptModel(t *testing.T) { func TestModelAdaptController(t *testing.T) { - sample := Controller{Name: "name", Fallback: "ok", Model: "", Cors: "*", Cache: ""} + sample := Controller{Name: "name", Fallback: "ok", Model: "", Cors: "*", Cache: "", Verb: "POST"} x, err := sample.adapt(nil) // Check if error is nil if err != nil { @@ -54,7 +54,7 @@ func TestModelAdaptController(t *testing.T) { } // Create an expected controller - expected := controller.Create("name", nil, []byte(`"ok"`), "*", "", "") + expected := controller.Create("name", nil, []byte(`"ok"`), "*", "", "POST") // Check if the adapted model is equal to the expected model using reflection if !reflect.DeepEqual(x, expected) { From caf208b975d95fab5b6f40f98245c1df49b775c8 Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Fri, 8 Mar 2024 06:18:28 +0100 Subject: [PATCH 12/14] added audit feature to cli --- Scaffold.go | 11 +++++++ cmd/cmd.go | 68 +++++++++++++++++++++++++++++++++++++++++++- docs/external/cli.md | 9 +++--- 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/Scaffold.go b/Scaffold.go index c0a0662..2ddfa76 100644 --- a/Scaffold.go +++ b/Scaffold.go @@ -43,6 +43,17 @@ func main() { } } + //audit a project + if len(os.Args) > 1 && os.Args[1] == "audit" { + if len(os.Args) > 2 { + cmd.Audit(os.Args[2]) + os.Exit(0) + } else { + fmt.Println("Error: Missing project name") + os.Exit(1) + } + } + // Run a scaffold app in current dir or a specified if len(os.Args) > 1 && os.Args[1] == "run" { entrypoint := "./main.yml" diff --git a/cmd/cmd.go b/cmd/cmd.go index fba755f..80adc32 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "service/configuration" + "service/misc" "slices" "strings" ) @@ -19,7 +20,8 @@ List of commands: version print out your scaffold version run run the scaffold from a config in a specified directory init creates a new project from a template - auto-doc generates api documentation for your app`) + auto-doc generates api documentation for your app + audit checks your project for potential error's'`) fmt.Println("\nTool by Dorian KalaczyƄski") os.Exit(0) @@ -133,3 +135,67 @@ func GenerateDoc(path string) { } }(file) } +func Audit(path string) { + // Generate the config struct first + conf, _ := configuration.Setup(path + "/main.yml") + + // Track seen names + seenNames := make(map[string]bool) + + // Track if any warnings were found + foundWarning := false + + // Check controllers + for _, controller := range conf.Controllers { + if !strings.HasSuffix(controller.Name, "_controller") { + fmt.Printf("Naming warning -> Controller %s should end with '_controller'\n", controller.Name) + foundWarning = true + } + if controller.Name != strings.ToLower(controller.Name) { + fmt.Printf("Naming warning -> Controller %s should be all lowercase\n", controller.Name) + foundWarning = true + } + if seenNames[controller.Name] { + fmt.Printf("Duplicate warning -> Controller %s is duplicated\n", controller.Name) + foundWarning = true + } else { + seenNames[controller.Name] = true + } + if slices.Equal(controller.Fallback, []byte("null")) { + fmt.Printf("General warning -> Controller %s has an empty fallback\n", controller.Name) + foundWarning = true + } + } + + // Check models + for _, model := range conf.Models { + if !strings.HasSuffix(model.Name, "_model") { + fmt.Printf("Naming warning -> Model %s should end with '_model'\n", model.Name) + foundWarning = true + } + if model.Name != strings.ToLower(model.Name) { + fmt.Printf("Naming warning -> Model %s should be all lowercase\n", model.Name) + foundWarning = true + } + + jsonT := model.GetJsonTemplate() + for _, Name := range jsonT.Keys() { + if Name != misc.Capitalize(Name) { + fmt.Printf("Naming warning -> Model %s has non-capitalized JSON field '%s'\n", model.Name, Name) + foundWarning = true + } + } + + if seenNames[model.Name] { + fmt.Printf("Duplicate warning -> Model %s is duplicated\n", model.Name) + foundWarning = true + } else { + seenNames[model.Name] = true + } + } + + // If no warnings were found, print a message + if !foundWarning { + fmt.Println("Success -> No warnings found during audit.") + } +} diff --git a/docs/external/cli.md b/docs/external/cli.md index 7873f99..c3eb12c 100644 --- a/docs/external/cli.md +++ b/docs/external/cli.md @@ -13,16 +13,17 @@ Start's your scaffold project, it requires a main.yml file to start. ```batch Scaffold run [project name] ``` -You can use this to run the current directory. -``` -Scaffold run . -``` ## init Create a new Scaffold project. ```batch Scaffold init [project name] ``` +## audit +Checks for naming issues and possible conflicts in a Scaffold project. +```batch +Scaffold audit [project name] +``` ## auto-doc (experimental feature) Automatically generates documentation in a auto-doc.md file,this gives a very rough documenation of the endpoint's. From 2d44c6b07108a95e9bf5e398c3fc1f3c45feff8b Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Sat, 9 Mar 2024 22:49:39 +0100 Subject: [PATCH 13/14] Added section about before deploying --- docs/external/good-practices.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/external/good-practices.md b/docs/external/good-practices.md index d576fe4..fff3356 100644 --- a/docs/external/good-practices.md +++ b/docs/external/good-practices.md @@ -2,7 +2,12 @@ --- Scaffold is a opinionated framework, naming decisions have already been made when it comes to naming. - +## Before Deploying +Before deploying your Scaffold app, it's a good idea to run these two commands to generate an auto-doc.md and find potential bug's. +```batch +Scaffold auto-doc [project name] +Scaffold audit [project name] +``` ## Naming Conventions --- From 0af22562bc5f43872e8bd4cd23d84f96e9a5e6c9 Mon Sep 17 00:00:00 2001 From: dorian3343 Date: Sat, 9 Mar 2024 22:51:56 +0100 Subject: [PATCH 14/14] Update version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 8ce995b..a92e827 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.0.3 \ No newline at end of file +v0.0.4 \ No newline at end of file