Basic Usage

This guide covers SmartRoute’s core features with practical examples derived from the test suite.

Overview

SmartRoute provides instance-scoped routing with hierarchical organization and plugin support. Each router instance is independent with its own plugin state.

Key concepts:

  • Routers are instantiated at runtime: Router(self, name="api")

  • Methods are marked with @route("router_name") decorator

  • Each instance gets isolated routing state

  • Plugins apply per-instance, not globally

Creating Your First Router

From test

Create a service with instance-scoped routing:

from smartroute import RoutedClass, Router, route

class Service(RoutedClass):
    def __init__(self, label: str):
        self.label = label
        self.api = Router(self, name="api")

    @route("api")
    def describe(self):
        return f"service:{self.label}"

# Each instance is isolated
first = Service("alpha")
second = Service("beta")

assert first.api.get("describe")() == "service:alpha"
assert second.api.get("describe")() == "service:beta"

Key points:

  • Router(self, name="api") creates instance-scoped router in __init__

  • @route("api") marks method for registration

  • RoutedClass mixin enables automatic router discovery and method registration

  • Each instance has independent routing state

Registering Handlers

From test

Methods are automatically registered when decorated with @route:

class API(RoutedClass):
    def __init__(self):
        self.routes = Router(self, name="routes")

    @route("routes")
    def echo(self, value: str):
        return value

    @route("routes", name="alt_name")
    def action(self):
        return "executed"

api = API()

# Direct name resolution
assert api.routes.get("echo")("hello") == "hello"

# Custom name resolution
assert api.routes.get("alt_name")() == "executed"

Registration happens automatically when you inherit from RoutedClass and instantiate routers in __init__.

Calling Handlers

From test

Use get() to retrieve handlers and call() for direct invocation:

class Calculator(RoutedClass):
    def __init__(self):
        self.ops = Router(self, name="ops")

    @route("ops")
    def add(self, a: int, b: int):
        return a + b

calc = Calculator()

# Via get() - returns callable
handler = calc.ops.get("add")
assert handler(2, 3) == 5

# Via call() - invokes directly
result = calc.ops.call("add", 10, 20)
assert result == 30

Difference:

  • get(name) returns the callable (for reuse)

  • call(name, *args, **kwargs) invokes immediately

Using Prefixes and Custom Names

From test

Clean up method names with prefixes and provide alternative names with the name option:

class SubService(RoutedClass):
    def __init__(self, prefix: str):
        self.prefix = prefix
        self.routes = Router(self, name="routes", prefix="handle_")

    @route("routes")
    def handle_list(self):
        return f"{self.prefix}:list"

    @route("routes", name="detail")
    def handle_detail(self, ident: int):
        return f"{self.prefix}:detail:{ident}"

sub = SubService("users")

# Prefix stripped: "handle_list" → "list"
assert sub.routes.get("list")() == "users:list"

# Custom name used: "handle_detail" → "detail"
assert sub.routes.get("detail")(10) == "users:detail:10"

Benefits:

  • Prefixes keep method names organized in code

  • Explicit names provide cleaner external APIs

  • Router resolves both automatically

Default Handlers

From test

Provide fallback handlers when routes don’t exist:

class Fallback(RoutedClass):
    def __init__(self):
        self.api = Router(self, name="api")

    @route("api")
    def known_action(self):
        return "success"

fb = Fallback()

# Existing handler
assert fb.api.get("known_action")() == "success"

# Non-existing with default
default_fn = lambda: "fallback"
assert fb.api.get("missing", default=default_fn)() == "fallback"

# Without default raises KeyError
try:
    fb.api.get("missing")()
except KeyError:
    pass  # Expected

Use defaults to:

  • Handle optional functionality gracefully

  • Provide “not found” handlers

  • Implement fallback behavior

Dynamic Handler Registration

From test

Add handlers programmatically at runtime:

class Dynamic(RoutedClass):
    def __init__(self):
        self.api = Router(self, name="api")

        # Register a lambda
        self.api.add_entry("greet", lambda name: f"Hello, {name}")

dyn = Dynamic()

# Dynamic handler works immediately
assert dyn.api.get("greet")("World") == "Hello, World"

Use cases:

  • Plugin-provided handlers

  • Configuration-driven routing

  • Runtime service composition

Building Hierarchies

From test

Create nested router structures with dotted path access:

class SubService(RoutedClass):
    def __init__(self, prefix: str):
        self.prefix = prefix
        self.routes = Router(self, name="routes", prefix="handle_")

    @route("routes")
    def handle_list(self):
        return f"{self.prefix}:list"

    @route("routes", name="detail")
    def handle_detail(self, ident: int):
        return f"{self.prefix}:detail:{ident}"

class RootAPI(RoutedClass):
    def __init__(self):
        self.api = Router(self, name="api")
        self.users = SubService("users")
        self.products = SubService("products")

        self.api.attach_instance(self.users, name="users")
        self.api.attach_instance(self.products, name="products")

root = RootAPI()

# Access with dotted paths
assert root.api.get("users.list")() == "users:list"
assert root.api.get("products.detail")(5) == "products:detail:5"

Hierarchies enable:

  • Organized service composition

  • Logical grouping of related handlers

  • Namespace isolation

Introspection

From test

Inspect router structure and registered handlers:

class Inspectable(RoutedClass):
    def __init__(self):
        self.api = Router(self, name="api")
        self.child_service = SubService("child")
        self.api.attach_instance(self.child_service, name="sub")

    @route("api")
    def action(self):
        pass

insp = Inspectable()

# Get metadata (single source: members)
info = insp.api.members()
assert "action" in info["handlers"]
assert "sub" in info["children"]

# Plugins can expose extra filters (e.g. scopes/channels from PublishPlugin)
internal_info = insp.api.members(scopes="internal", channel="CLI")

# Filters likewise depend on plugin metadata
internal = insp.api.members(scopes="internal")
internal_cli = insp.api.members(scopes="internal", channel="CLI")

Use members() to:

  • Generate API documentation

  • Debug routing issues

  • Validate configuration

Next Steps

Now that you understand the basics: