Skip to content

Guide Services

Kris Simon edited this page Dec 31, 2025 · 3 revisions

External Services

ARO integrates with external libraries through Services. Services wrap external functionality (HTTP clients, databases, media processors, etc.) and expose them through the <Call> action.

The Call Action

All external service invocations use the same pattern:

<Call> the <result> from the <service: method> with { key: value, ... }.
Component Description
result Variable to store the result
service Service name (e.g., postgres, ffmpeg, redis)
method Method to invoke (e.g., query, execute, transcode)
args Key-value arguments

Creating Custom Services

Services are Swift types that implement the AROService protocol.

Service Protocol

public protocol AROService: Sendable {
    /// Service name (e.g., "postgres", "redis")
    static var name: String { get }

    /// Initialize the service
    init() throws

    /// Call a method
    func call(_ method: String, args: [String: any Sendable]) async throws -> any Sendable

    /// Shutdown (optional)
    func shutdown() async
}

Example: PostgreSQL Service

import PostgresNIO

public struct PostgresService: AROService {
    public static let name = "postgres"

    private let pool: PostgresConnectionPool

    public init() throws {
        let config = PostgresConnection.Configuration(...)
        pool = try PostgresConnectionPool(configuration: config)
    }

    public func call(_ method: String, args: [String: any Sendable]) async throws -> any Sendable {
        switch method {
        case "query":
            let sql = args["sql"] as! String
            let rows = try await pool.query(sql)
            return rows.map { row in
                // Convert to dictionary
            }

        case "execute":
            let sql = args["sql"] as! String
            try await pool.execute(sql)
            return ["success": true]

        default:
            throw ServiceError.unknownMethod(method, service: Self.name)
        }
    }

    public func shutdown() async {
        await pool.close()
    }
}

Registration

Services are registered with the ServiceRegistry:

try ServiceRegistry.shared.register(PostgresService())

Usage in ARO

(* Database query *)
<Call> the <users> from the <postgres: query> with {
    sql: "SELECT * FROM users WHERE active = true"
}.

(* Database execute *)
<Call> the <result> from the <postgres: execute> with {
    sql: "UPDATE users SET status = 'active' WHERE id = 123"
}.

Plugin System

When ARO is distributed as a pre-compiled binary, users can add custom services via plugins.

Plugin Structure

Plugins can be either single Swift files or Swift packages with dependencies:

Simple Plugin (single file):

MyApp/
├── main.aro
├── openapi.yaml
└── plugins/
    └── MyService.swift

Package Plugin (with dependencies):

MyApp/
├── main.aro
├── openapi.yaml
└── plugins/
    └── MyPlugin/
        ├── Package.swift
        └── Sources/MyPlugin/
            └── MyService.swift

Writing a Plugin

Plugins use a C-compatible JSON interface:

// plugins/GreetingService.swift
import Foundation

/// Plugin initialization - returns service metadata as JSON
@_cdecl("aro_plugin_init")
public func pluginInit() -> UnsafePointer<CChar> {
    let metadata = """
    {"services": [{"name": "greeting", "symbol": "greeting_call"}]}
    """
    return UnsafePointer(strdup(metadata)!)
}

/// Service entry point - C-callable interface
@_cdecl("greeting_call")
public func greetingCall(
    _ methodPtr: UnsafePointer<CChar>,
    _ argsPtr: UnsafePointer<CChar>,
    _ resultPtr: UnsafeMutablePointer<UnsafeMutablePointer<CChar>?>
) -> Int32 {
    let method = String(cString: methodPtr)
    let argsJSON = String(cString: argsPtr)

    // Parse arguments
    var args: [String: Any] = [:]
    if let data = argsJSON.data(using: .utf8),
       let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
        args = parsed
    }

    // Execute method
    let name = args["name"] as? String ?? "World"
    let result: String

    switch method.lowercased() {
    case "hello":
        result = "Hello, \(name)!"
    case "goodbye":
        result = "Goodbye, \(name)!"
    default:
        let errorJSON = "{\"error\": \"Unknown method: \(method)\"}"
        resultPtr.pointee = strdup(errorJSON)
        return 1
    }

    // Return result as JSON
    let resultJSON = "{\"result\": \"\(result)\"}"
    resultPtr.pointee = strdup(resultJSON)
    return 0
}

Package Plugin with Dependencies

For plugins that need external libraries, use a Swift package:

// plugins/ZipPlugin/Package.swift
// swift-tools-version:5.9
import PackageDescription

let package = Package(
    name: "ZipPlugin",
    platforms: [.macOS(.v13)],
    products: [
        .library(name: "ZipPlugin", type: .dynamic, targets: ["ZipPlugin"])
    ],
    dependencies: [
        .package(url: "https://github.com/marmelroy/Zip.git", from: "2.1.0")
    ],
    targets: [
        .target(name: "ZipPlugin", dependencies: ["Zip"])
    ]
)

How Plugins Work

  1. ARO scans ./plugins/ directory
  2. For .swift files: compiles to .dylib using swiftc
  3. For directories with Package.swift: builds using swift build
  4. Loads dynamic library via dlopen
  5. Calls aro_plugin_init to get service metadata (JSON)
  6. Registers each service with the symbol from metadata

Compiled plugins are cached in .aro-cache/ and only recompiled when source changes.

Plugin Metadata Format

The aro_plugin_init function returns JSON:

{
  "services": [
    {"name": "greeting", "symbol": "greeting_call"}
  ]
}
  • name: Service name used in ARO code (<greeting: hello>)
  • symbol: C function symbol to call

Using Plugin Services

(Application-Start: Plugin Demo) {
    <Call> the <greeting> from the <myservice: greet> with {
        name: "ARO Developer"
    }.

    <Log> <greeting> to the <console>.

    <Return> an <OK: status> for the <startup>.
}

Common Service Examples

External API (using Request action)

(Fetch Weather: External API) {
    (* Use the built-in Request action for HTTP calls *)
    <Request> the <weather> from "https://api.weather.com/current" with {
        headers: { "Authorization": "Bearer ${API_KEY}" }
    }.

    <Return> an <OK: status> with <weather>.
}

Note: HTTP requests use the built-in Request action, not Call. See Actions for details.

Database Query

(List Users: User Management) {
    <Call> the <users> from the <postgres: query> with {
        sql: "SELECT * FROM users WHERE active = true"
    }.

    <Return> an <OK: status> with <users>.
}

Media Processing

(Generate Thumbnail: Media) {
    <Extract> the <video-path> from the <request: path>.

    <Call> the <thumbnail> from the <ffmpeg: extractFrame> with {
        input: <video-path>,
        time: "00:00:05",
        output: "/tmp/thumb.jpg"
    }.

    <Return> an <OK: status> with <thumbnail>.
}

Design Philosophy

  1. One Action, Many Services: All external calls use <Call>
  2. Swift-First: Services are Swift types, leveraging the Swift ecosystem
  3. Package-Based: Services are Swift Packages, easy to create and share
  4. Works Everywhere: Same approach for interpreter and compiler modes

Next Steps

Clone this wiki locally