Skip to content
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
4 changes: 2 additions & 2 deletions examples/real-project/starter/scripts_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 2 additions & 4 deletions examples/real-project/starter/scripts_endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/real-project/starter/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
3 changes: 3 additions & 0 deletions examples/scripts/scripts/some_script.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 1 addition & 2 deletions scripting/kind.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...")
Expand Down
88 changes: 82 additions & 6 deletions scripting/scripting.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,107 @@ 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
Collector func([]string) interface{}
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{
Name: name,
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
},
}
}