Native Classes

Create Go classes that can be instantiated and used from Scriptling using the Native API.

Basic Class

A class is an *object.Class structure containing methods:

import (
    "context"
    "fmt"
    "github.com/paularlott/scriptling"
    "github.com/paularlott/scriptling/object"
)

var PersonClass = &object.Class{
    Name: "Person",
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                // args[0] is always 'self' (the instance)
                instance := args[0].(*object.Instance)

                name, _ := args[1].AsString()
                age, _ := args[2].AsInt()

                instance.Fields["name"] = &object.String{Value: name}
                instance.Fields["age"] = object.NewInteger(age)

                return object.None
            },
            HelpText: "__init__(name, age) - Initialize Person",
        },
        "greet": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)

                name, _ := instance.Fields["name"].AsString()
                return &object.String{Value: "Hello, " + name + "!"}
            },
            HelpText: "greet() - Return greeting with person's name",
        },
        "birthday": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)

                age, _ := instance.Fields["age"].AsInt()
                instance.Fields["age"] = object.NewInteger(age + 1)

                return &object.String{Value: fmt.Sprintf("Happy birthday! You're now %d", age+1)}
            },
            HelpText: "birthday() - Increment age and return birthday message",
        },
        "get_info": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)

                name, _ := instance.Fields["name"].AsString()
                age, _ := instance.Fields["age"].AsInt()

                return object.NewStringDict(map[string]object.Object{
                    "name": &object.String{Value: name},
                    "age":  object.NewInteger(age),
                })
            },
            HelpText: "get_info() - Return person info as dict",
        },
    },
}

func main() {
    p := scriptling.New()

    // Register class
    p.SetVar("Person", PersonClass)

    // Use from Scriptling
    p.Eval(`
person = Person("Alice", 30)
print(person.greet())      # "Hello, Alice!"
print(person.birthday())   # "Happy birthday! You're now 31"
info = person.get_info()
`)
}

Creating Instances from Go

func main() {
    p := scriptling.New()
    p.SetVar("Person", PersonClass)

    // Create instance from Go
    instance, err := p.CreateInstance("Person", "Bob", 25)
    if err != nil {
        log.Fatal(err)
    }

    // Store instance in variable
    p.SetObjectVar("bob", instance)

    // Call methods
    greeting, _ := p.CallMethod(instance, "greet")
    fmt.Println(greeting.Inspect())  // "Hello, Bob!"

    // Use from script
    p.Eval(`
print(bob.greet())
bob.birthday()
`)
}

The init Method

The __init__ method is the constructor, called when creating a new instance:

var RectangleClass = &object.Class{
    Name: "Rectangle",
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                if len(args) < 3 {
                    return &object.Error{Message: "__init__ requires instance, width, and height"}
                }
                instance := args[0].(*object.Instance)
                width, _ := args[1].AsFloat()
                height, _ := args[2].AsFloat()

                instance.Fields["width"] = object.NewFloat(width)
                instance.Fields["height"] = object.NewFloat(height)
                return object.None
            },
            HelpText: "__init__(width, height) - Initialize Rectangle",
        },
        "area": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                width, _ := instance.Fields["width"].AsFloat()
                height, _ := instance.Fields["height"].AsFloat()
                return object.NewFloat(width * height)
            },
            HelpText: "area() - Calculate area",
        },
        "perimeter": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                width, _ := instance.Fields["width"].AsFloat()
                height, _ := instance.Fields["height"].AsFloat()
                return object.NewFloat(2 * (width + height))
            },
            HelpText: "perimeter() - Calculate perimeter",
        },
    },
}

Inheritance

Base Class

var AnimalClass = &object.Class{
    Name: "Animal",
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                name, _ := args[1].AsString()
                instance.Fields["name"] = &object.String{Value: name}
                return object.None
            },
            HelpText: "__init__(name) - Initialize Animal",
        },
        "speak": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                return &object.String{Value: "Generic animal sound"}
            },
            HelpText: "speak() - Make animal sound",
        },
        "info": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                name, _ := instance.Fields["name"].AsString()
                return &object.String{Value: "Animal: " + name}
            },
            HelpText: "info() - Return animal info",
        },
    },
}

Derived Class

var DogClass = &object.Class{
    Name: "Dog",
    BaseClass: AnimalClass,  // Inherit from Animal
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                name, _ := args[1].AsString()
                breed, _ := args[2].AsString()

                // Call parent __init__
                animalInit := AnimalClass.Methods["__init__"].(*object.Builtin)
                animalInit.Fn(ctx, nil, instance, &object.String{Value: name})

                instance.Fields["breed"] = &object.String{Value: breed}
                return object.None
            },
            HelpText: "__init__(name, breed) - Initialize Dog",
        },
        "speak": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                return &object.String{Value: "Woof!"}
            },
            HelpText: "speak() - Bark",
        },
        "fetch": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                name, _ := instance.Fields["name"].AsString()
                return &object.String{Value: name + " fetches the ball!"}
            },
            HelpText: "fetch() - Fetch something",
        },
    },
}

Using Inheritance

func main() {
    p := scriptling.New()
    p.SetVar("Animal", AnimalClass)
    p.SetVar("Dog", DogClass)

    p.Eval(`
dog = Dog("Rex", "German Shepherd")
print(dog.speak())   # "Woof!" (overridden method)
print(dog.fetch())   # "Rex fetches the ball!"
print(dog.info())    # "Animal: Rex" (inherited method)
`)
}

Special Methods

Full Dunder Method Reference

Method Purpose
__init__ Constructor called when creating instances
__str__ String representation — used by str() and f-strings
__repr__ Debug representation — used by repr()
__len__ Length — used by len()
__bool__ Truthiness — falls back to __len__ if absent
__iter__ Return an iterator object
__next__ Return next value; raise StopIteration when done
__contains__ Membership test — used by in operator
__eq__ Equality (==)
__ne__ Inequality (!=)
__lt__ Less-than (<) — also used by sorted()
__gt__ Greater-than (>)
__le__ Less-than-or-equal (<=)
__ge__ Greater-than-or-equal (>=)
__enter__ Context manager entry — called by with
__exit__ Context manager exit — always called; return truthy to suppress exceptions
__getitem__ Custom indexing — used by obj[key]

All dunder methods are inherited through the class hierarchy.

__getitem__(key) - Custom Indexing

counterClass := &object.Class{
    Name: "Counter",
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                instance.Fields["data"] = object.NewStringDict(map[string]object.Object{})
                return object.None
            },
        },
        "__getitem__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                key := args[1].Inspect()
                data := instance.Fields["data"].(*object.Dict)
                if pair, ok := data.GetByString(key); ok {
                    return pair.Value
                }
                return &object.Integer{Value: 0}  // Default for missing keys
            },
            HelpText: "__getitem__(key) - Get count for key",
        },
        "set": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                key := args[1].Inspect()
                value := args[2]
                data := instance.Fields["data"].(*object.Dict)
                data.SetByString(key, value)
                return object.None
            },
            HelpText: "set(key, value) - Set a count",
        },
    },
}

// Enables: c[key] syntax
p.Eval(`
c = Counter()
c.set("apples", 5)
print(c["apples"])   # 5
print(c["oranges"])  # 0 (default)
`)

Note: Use object.NewStringDict() to create dicts and GetByString()/SetByString() for access. Never manipulate the internal Pairs map keys directly — they use a type-prefixed canonical format.

Properties and Static Methods

Wrap methods in object.Property or object.StaticMethod to get the same behaviour as @property and @staticmethod in Scriptling scripts.

object.Property

The Getter is called with self as the only argument when the attribute is accessed (no call parens needed from the script). Add a Setter to allow assignment:

var CircleClass = &object.Class{
    Name: "Circle",
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                r, _ := args[1].AsFloat()
                instance.Fields["radius"] = &object.Float{Value: r}
                return object.None
            },
        },
        "radius": &object.Property{
            Getter: &object.Builtin{
                Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                    instance := args[0].(*object.Instance)
                    return instance.Fields["radius"]
                },
            },
            Setter: &object.Builtin{
                Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                    instance := args[0].(*object.Instance)
                    instance.Fields["radius"] = args[1]
                    return &object.Null{}
                },
            },
        },
        "area": &object.Property{  // read-only: no Setter
            Getter: &object.Builtin{
                Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                    instance := args[0].(*object.Instance)
                    r, _ := instance.Fields["radius"].AsFloat()
                    return &object.Float{Value: 3.14159 * r * r}
                },
            },
        },
    },
}

// c = Circle(5.0)
// print(c.radius)  # 5  — no parens
// c.radius = 10    # calls setter
// print(c.area)    # read-only, assignment raises error

object.StaticMethod

The Fn is called without self. Callable on both the class and instances:

var MathClass = &object.Class{
    Name: "Math",
    Methods: map[string]object.Object{
        "square": &object.StaticMethod{
            Fn: &object.Builtin{
                Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                    v, _ := args[0].AsFloat()
                    return object.NewFloat(v * v)
                },
            },
        },
    },
}

// Math.square(4)  # 16
// m = Math()
// m.square(4)     # 16

Classes in Libraries

Add classes to libraries via the constants map:

myLib := object.NewLibrary("counters",
    map[string]*object.Builtin{
        "create_counter": {
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                // Factory function
                return &object.Instance{
                    Class: counterClass,
                    Fields: map[string]object.Object{
                        "data": object.NewStringDict(map[string]object.Object{}),
                    },
                }
            },
            HelpText: "create_counter() - Create a new Counter",
        },
    },
    map[string]object.Object{
        "Counter": counterClass,           // Expose for direct instantiation
        "VERSION": &object.String{Value: "1.0.0"},
    },
    "Counter utilities library",
)

p.RegisterLibrary(myLib)

// Use in Scriptling
p.Eval(`
import counters

# Use factory
c = counters.create_counter()

# Or use class directly
c2 = counters.Counter()
`)

Complete Example: HTTP Client Class

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"

    "github.com/paularlott/scriptling"
    "github.com/paularlott/scriptling/object"
)

var HTTPClientClass = &object.Class{
    Name: "HTTPClient",
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)

                // Get optional parameters
                baseURL, _ := kwargs.GetString("base_url", "")
                timeout, _ := kwargs.GetInt("timeout", 30)

                instance.Fields["base_url"] = &object.String{Value: baseURL}
                instance.Fields["timeout"] = object.NewInteger(int64(timeout))
                instance.Fields["headers"] = object.NewStringDict(map[string]object.Object{})

                return object.None
            },
            HelpText: "__init__(base_url='', timeout=30) - Create HTTP client",
        },
        "set_header": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                key, _ := args[1].AsString()
                value, _ := args[2].AsString()

                headers := instance.Fields["headers"].(*object.Dict)
                headers.SetByString(key, &object.String{Value: value})

                return object.None
            },
            HelpText: "set_header(key, value) - Set default header",
        },
        "get": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                path, _ := args[1].AsString()

                // Build URL
                baseURL, _ := instance.Fields["base_url"].AsString()
                url := baseURL + path

                // Get timeout
                timeoutSec, _ := instance.Fields["timeout"].AsInt()

                // Create client with timeout
                client := &http.Client{
                    Timeout: time.Duration(timeoutSec) * time.Second,
                }

                // Create request
                req, err := http.NewRequest("GET", url, nil)
                if err != nil {
                    return &object.Error{Message: err.Error()}
                }

                // Add headers
                headers := instance.Fields["headers"].(*object.Dict)
                for _, pair := range headers.Pairs {
                    key := pair.Key.Inspect()
                    valStr, _ := pair.Value.AsString()
                    req.Header.Set(key, valStr)
                }

                // Execute
                resp, err := client.Do(req)
                if err != nil {
                    return &object.Error{Message: err.Error()}
                }
                defer resp.Body.Close()

                body, _ := io.ReadAll(resp.Body)

                return object.NewStringDict(map[string]object.Object{
                    "status":  object.NewInteger(int64(resp.StatusCode)),
                    "body":    &object.String{Value: string(body)},
                    "headers": object.NewStringDict(map[string]object.Object{}),
                })
            },
            HelpText: "get(path) - Make GET request",
        },
    },
}

func main() {
    p := scriptling.New()
    p.SetVar("HTTPClient", HTTPClientClass)

    p.Eval(`
client = HTTPClient(base_url="https://api.example.com", timeout=10)
client.set_header("Authorization", "Bearer token123")
client.set_header("Content-Type", "application/json")

response = client.get("/users")
if response["status"] == 200:
    print("Success!")
    print(response["body"])
else:
    print("Error:", response["status"])
`)
}

Best Practices

1. Always Handle self

First argument is always the instance:

Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    instance := args[0].(*object.Instance)  // self
    // ... rest of implementation
}

2. Use instance.Fields for State

Store instance data in the Fields map:

instance.Fields["name"] = &object.String{Value: name}
instance.Fields["count"] = object.NewInteger(count)

3. Return object.None for Void Methods

Methods without return values should return None:

Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    // ... implementation
    return object.None
}

4. Use Type Assertions Safely

Check types before casting:

// Safe type handling
func safeMethod(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    if len(args) < 2 {
        return &object.Error{Message: "method requires at least 1 argument"}
    }

    instance, ok := args[0].(*object.Instance)
    if !ok {
        return &object.Error{Message: "invalid instance"}
    }

    value, err := args[1].AsString()
    if err != nil {
        return &object.Error{Message: "argument must be a string"}
    }

    // Safe to use instance and value
    instance.Fields["data"] = &object.String{Value: value}
    return object.None
}

Testing Classes

func TestClass(t *testing.T) {
    p := scriptling.New()

    // Create class
    counterClass := &object.Class{
        Name: "Counter",
        Methods: map[string]object.Object{
            "__init__": &object.Builtin{
                Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                    instance := args[0].(*object.Instance)
                    instance.Fields["count"] = &object.Integer{Value: 0}
                    return object.None
                },
            },
            "increment": &object.Builtin{
                Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                    instance := args[0].(*object.Instance)
                    count, _ := instance.Fields["count"].AsInt()
                    instance.Fields["count"] = object.NewInteger(count + 1)
                    return object.NewInteger(count + 1)
                },
            },
        },
    }

    // Register class
    p.SetVar("Counter", counterClass)

    // Test the class
    result, err := p.Eval(`
c = Counter()
c.increment()
c.increment()
result = c.increment()
`)
    if err != nil {
        t.Fatalf("Eval error: %v", err)
    }

    if value, objErr := result.AsInt(); objErr == nil {
        if value != 3 {
            t.Errorf("Expected 3, got %d", value)
        }
    }
}

See Also