Skip to content

Guide Services

Kris Simon edited this page Mar 1, 2026 · 4 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