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

Merge v0.0.4 with main #7

Merged
merged 16 commits into from
Mar 9, 2024
Merged
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
11 changes: 11 additions & 0 deletions Scaffold.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.0.3
v0.0.4
78 changes: 77 additions & 1 deletion cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path/filepath"
"service/configuration"
"service/misc"
"slices"
"strings"
)
Expand All @@ -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)

Expand Down Expand Up @@ -106,6 +108,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")
}
}
Expand All @@ -123,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.")
}
}
28 changes: 19 additions & 9 deletions components/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ type Controller struct {
Model *model.Model
Fallback []byte
cors 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) Controller {
return Controller{Name: name, Model: datamodel, Fallback: fallback, cors: cors}
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) {
Expand All @@ -39,17 +41,24 @@ 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)
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 {
c.handleNoModelRequest(w)
} else {
Expand All @@ -61,7 +70,8 @@ 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)
// make the db query
query, err := c.Model.Querybuilder(body)
if err != nil {
if err.Error() == "JSON request does not match spec" {
Expand All @@ -74,8 +84,8 @@ func (c Controller) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
}

// Make the db query
log.Trace().Msg("Running Query in : " + c.Name)
// Queries the database
result, err := c.Model.Query(query)
if err != nil {
log.Err(err).Msg("Something went wrong with querying database")
Expand Down Expand Up @@ -120,6 +130,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)
Expand All @@ -139,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))
Expand Down
12 changes: 6 additions & 6 deletions components/controller/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
8 changes: 6 additions & 2 deletions components/model/Model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -37,13 +40,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
}
Expand Down Expand Up @@ -75,7 +80,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 {
Expand Down
1 change: 0 additions & 1 deletion components/model/TypeSpecGeneration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion configuration/Config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 12 additions & 1 deletion configuration/ConfigPrimitiveTypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -35,14 +36,24 @@ type Controller struct {
Name string `yaml:"name"`
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) {
JSON, err := json.Marshal(c.Fallback)
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
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
Expand Down
4 changes: 2 additions & 2 deletions configuration/adapter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ 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: "", Verb: "POST"}
x, err := sample.adapt(nil)
// Check if error is nil
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}

// 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) {
Expand Down
5 changes: 3 additions & 2 deletions docs/external/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading