diff --git a/examples/real-project/starter/scripts_database.go b/examples/real-project/starter/scripts_database.go index 26f4686..a2aad33 100644 --- a/examples/real-project/starter/scripts_database.go +++ b/examples/real-project/starter/scripts_database.go @@ -11,7 +11,7 @@ import ( // Script to reset the database by dropping and recreating all tables // // Here we just use any to ignore the argument. This can be useful for scripts such as this one. -func ResetDatabase(runner *mrunner.Runner, _ any) error { +func ResetDatabase(runner *mrunner.Runner) error { log.Println("Resetting database...") // Magic can clear all databases for you, don't worry, only data will be deleted meaning your schema is still all good :D @@ -31,7 +31,7 @@ var SamplePosts = []database.Post{ // Script to seed the database with sample posts // // Here we just use any to ignore the argument. This can be useful for scripts such as this one. -func SeedDatabase(runner *mrunner.Runner, _ any) error { +func SeedDatabase() error { log.Println("Seeding database with sample posts...") // Connect to the database diff --git a/examples/real-project/starter/scripts_endpoints.go b/examples/real-project/starter/scripts_endpoints.go index c548c25..bdeb895 100644 --- a/examples/real-project/starter/scripts_endpoints.go +++ b/examples/real-project/starter/scripts_endpoints.go @@ -6,8 +6,6 @@ import ( "os" "real-project/database" "real-project/util" - - "github.com/Liphium/magic/mrunner" ) // This method would ideally be created in a shared package between all the scripts. @@ -18,13 +16,13 @@ func GetPath() string { // Script for creating a post using the endpoint. // // You could go into the database and add it there, but we want to be able to call the endpoint using scripts. -func CreatePost(_ *mrunner.Runner, post database.Post) error { +func CreatePost(post database.Post) error { _, err := util.Post[interface{}](GetPath()+"/posts", post, util.Headers{}) return err } // Script for printing all the posts using the endpoint. -func PrintPosts(_ *mrunner.Runner, _ any) error { +func PrintPosts() error { posts, err := util.Get[[]database.Post](GetPath()+"/posts", util.Headers{}) if err != nil { return err diff --git a/examples/real-project/starter/start_test.go b/examples/real-project/starter/start_test.go index 8e23ae5..921720b 100644 --- a/examples/real-project/starter/start_test.go +++ b/examples/real-project/starter/start_test.go @@ -56,7 +56,7 @@ func TestApp(t *testing.T) { magic.GetTestRunner().ClearDatabases() // Yes, you can call scripts in here to make your life a little easier. - if err := starter.SeedDatabase(magic.GetTestRunner(), ""); err != nil { + if err := starter.SeedDatabase(); err != nil { t.Fatal("Couldn't seed database:", err) } diff --git a/examples/scripts/scripts/some_script.go b/examples/scripts/scripts/some_script.go index 6aadb0c..77ad718 100644 --- a/examples/scripts/scripts/some_script.go +++ b/examples/scripts/scripts/some_script.go @@ -13,6 +13,9 @@ type SomeScriptOptions struct { // This is the main entrypoint for your script. // Run it with go run . -r some_script +// +// You can get the runner here if you need it. But you could also just delete the parameter and the code +// would just work the same. func SomeScript(runner *mrunner.Runner, data SomeScriptOptions) error { log.Println("chosen name:", data.Name) log.Println("chosen email:", data.Email) diff --git a/scripting/kind.go b/scripting/kind.go index ac6b52b..3ad8749 100644 --- a/scripting/kind.go +++ b/scripting/kind.go @@ -25,14 +25,13 @@ var SupportedKinds = append([]reflect.Kind{ }, numberKindsSupported...) // Create a function that can take a struct -func CreateCollector[T any]() (func([]string) interface{}, error) { +func CreateCollector(genType reflect.Type) (func([]string) interface{}, error) { // Create the validator in case it's not there yet if validate == nil { validate = createValidator() } - genType := reflect.TypeFor[T]() if genType.Kind() != reflect.Struct { if mconfig.VerboseLogging { util.Log.Println("Ignoring script argument due to not being a struct...") diff --git a/scripting/scripting.go b/scripting/scripting.go index 17c6b70..9267025 100644 --- a/scripting/scripting.go +++ b/scripting/scripting.go @@ -2,12 +2,11 @@ package scripting import ( "log" + "reflect" "github.com/Liphium/magic/mrunner" ) -type ScriptFunction[T any] = func(*mrunner.Runner, T) error - type Script struct { Name string Description string @@ -15,10 +14,46 @@ type Script struct { Handler func(*mrunner.Runner, interface{}) error } -func CreateScript[T any](name string, description string, f ScriptFunction[T]) Script { - collector, err := CreateCollector[T]() +// f must be of type func(*mrunner.Runner, T, ...) (..., error). +// +// You can have any return types for f and the second parameter, T, can be a struct that will be dynamically generated +// based on CLI arguments or a CLI form. Otherwise it will be ignored. Read more about it in the documentation. +func CreateScript(name string, description string, f interface{}) Script { + + // Make sure f is a function + scriptType := reflect.TypeOf(f) + if scriptType.Kind() != reflect.Func { + log.Fatalf("No function is provided for script %s.", name) + } + + // Find the runner parameter position (can be anywhere, or not exist) + runnerPos := -1 + runnerType := reflect.TypeFor[*mrunner.Runner]() + for i := 0; i < scriptType.NumIn(); i++ { + if scriptType.In(i) == runnerType { + runnerPos = i + break + } + } + + // Find the first parameter that's not the runner (this will be collected) + collectionPos := -1 + collectionType := reflect.TypeFor[any]() + for i := 0; i < scriptType.NumIn(); i++ { + if i != runnerPos { + collectionPos = i + collectionType = scriptType.In(i) + break + } + } + collector, err := CreateCollector(collectionType) if err != nil { - log.Fatalln("couldn't create collector:", err) + log.Fatalln("Something went wrong with internally: Couldn't create collector:", err) + } + + // Enforce last return value being an error + if scriptType.NumOut() < 1 || scriptType.Out(scriptType.NumOut()-1).Name() != reflect.TypeFor[error]().Name() { + log.Fatalf("Last return type of script %s isn't an error.", name) } return Script{ @@ -26,7 +61,48 @@ func CreateScript[T any](name string, description string, f ScriptFunction[T]) S Description: description, Collector: collector, Handler: func(runner *mrunner.Runner, data interface{}) error { - return f(runner, data.(T)) + // Use reflection to call the function f + scriptValue := reflect.ValueOf(f) + + // Prepare arguments for all parameters + args := make([]reflect.Value, scriptType.NumIn()) + + // Set the runner parameter if it exists + if runnerPos != -1 { + args[runnerPos] = reflect.ValueOf(runner) + } + + // Set the collected parameter if it exists + if collectionPos != -1 { + dataValue := reflect.ValueOf(data) + if dataValue.Type().ConvertibleTo(collectionType) { + args[collectionPos] = dataValue.Convert(collectionType) + } else { + args[collectionPos] = dataValue + } + } + + // Fill remaining parameters with zero values + for i := 0; i < scriptType.NumIn(); i++ { + if i != runnerPos && i != collectionPos { + args[i] = reflect.Zero(scriptType.In(i)) + } + } + + // Call the function + results := scriptValue.Call(args) + + // Check if the function returns an error (last return value should be error) + if len(results) > 0 { + lastResult := results[len(results)-1] + if lastResult.Type().Implements(reflect.TypeFor[error]()) { + if !lastResult.IsNil() { + return lastResult.Interface().(error) + } + } + } + + return nil }, } }