Skip to content

Guide Services

Kris edited this page Apr 17, 2026 · 5 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

Use the AROPluginKit SDK with the @AROExport macro and the .service() builder:

// plugins/GreetingService/Sources/GreetingService/GreetingService.swift
import AROPluginKit

@AROExport
private let plugin = AROPlugin(name: "greeting-service", version: "1.0.0", handle: "Greeting")
    .service("greeting", methods: ["hello", "goodbye"]) { method, input in
        let name = input.with.string("name") ?? "World"

        switch method.lowercased() {
        case "hello":
            return .success(["result": "Hello, \(name)!"])
        case "goodbye":
            return .success(["result": "Goodbye, \(name)!"])
        default:
            return .failure(.notFound, "Unknown method: \(method)")
        }
    }

The @AROExport macro and the SDK generate all C ABI exports automatically — no manual @_cdecl, no unsafe pointers, no strdup.

Package Plugin with Dependencies

For plugins that need external libraries, add a Package.swift:

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

let package = Package(
    name: "ZipPlugin",
    platforms: [.macOS(.v12)],
    products: [
        .library(name: "ZipPlugin", type: .dynamic, targets: ["ZipPlugin"])
    ],
    dependencies: [
        .package(url: "https://github.com/marmelroy/Zip.git", from: "2.1.0"),
        .package(url: "https://github.com/arolang/aro-plugin-sdk-swift.git", branch: "main"),
    ],
    targets: [
        .target(name: "ZipPlugin", dependencies: [
            "Zip",
            .product(name: "AROPluginKit", package: "aro-plugin-sdk-swift"),
        ])
    ]
)

How Plugins Work

  1. ARO scans the Plugins/ directory for subdirectories with plugin.yaml
  2. For Package.swift plugins: builds using swift build and loads the .dylib/.so
  3. For single-file .swift plugins: compiles directly with swiftc
  4. Calls aro_plugin_register (generated by @AROExport) to trigger initialization
  5. Calls aro_plugin_info (generated by the SDK) to get plugin metadata
  6. Registers actions, qualifiers, and services declared in the metadata

Built plugins are cached and only recompiled when source changes.

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