← Back to Documentation

Writing Extensions

Create your own ARO packages with custom actions, feature sets, and native plugins. Share them with the community or use them in your projects.

Package Structure

An ARO package is a directory with a plugin.yaml manifest and source files. The manifest is required - without it, ARO won't recognize the directory as a package.

my-package/
├── plugin.yaml              # Required: Package manifest
├── README.md                # Optional: Documentation
├── features/                # ARO feature sets (.aro files)
│   ├── csv-parser.aro
│   └── csv-formatter.aro
├── Sources/                 # Native plugins (Swift, Rust, C)
│   └── CSVParser.swift
└── tests/                   # Test files
    └── csv-parser.test.aro

The plugin.yaml Manifest

The plugin.yaml file describes your package. It's the only required file and contains all metadata ARO needs to load your package.

# plugin.yaml - Package manifest
name: csv-tools
version: 1.0.0
handle: CSV                   # PascalCase namespace for actions and qualifiers
description: "CSV parsing, validation, and formatting tools"
author: "Your Name"
license: MIT
aro-version: ">=0.2.0"

# What this package provides
provides:
  - type: aro-files           # ARO feature sets
    path: features/
  - type: swift-plugin        # Swift native code
    path: Sources/

# System library requirements (optional)
system:
  - libsqlite3

# Dependencies on other packages (optional)
dependencies:
  aro-core-utils:
    git: "git@github.com:arolang/core-utils.git"
    ref: "v2.0.0"

# Build configuration (optional)
build:
  swift:
    minimum-version: "6.2"
    targets:
      - name: CSVTools
        path: Sources/

When installed via aro add, a source: block is automatically added to record the Git URL, ref, and commit hash.

Required Fields

FieldDescription
nameUnique package identifier
versionSemantic version (e.g., 1.0.0)
handlePascalCase namespace (e.g., CSV). Actions are invoked as Handle.Action, qualifiers as handle.qualifier
providesList of content types and paths

Optional Fields

FieldDescription
descriptionHuman-readable description
authorAuthor name
licenseLicense identifier (e.g., MIT)
aro-versionRequired ARO version constraint (e.g., >=0.2.0)
systemRequired system libraries (e.g., [libsqlite3])
dependenciesOther ARO packages this plugin depends on
buildLanguage-specific build configuration

Provides Types

TypeDescription
aro-filesARO feature sets (.aro files)
swift-pluginSwift native plugins
rust-pluginRust native plugins
c-pluginC native plugins
cpp-pluginC++ native plugins
python-pluginPython plugins
aro-templatesHTML templates

Pure ARO Packages

The simplest packages contain only .aro files. These provide reusable feature sets without any native code:

(* features/csv-parser.aro *)

(Parse CSV File: CSV Processing) {
    Extract the <content> from the <file>.
    Split the <lines> from the <content> by "\n".
    Extract the <header: first> from the <lines>.
    Split the <columns> from the <header> by ",".
    Transform each <row> in the <lines: 1-> with <columns>.
    Return an <OK: status> with <rows>.
}

(Validate CSV Schema: CSV Validation) {
    Extract the <data> from the <input: data>.
    Extract the <schema> from the <input: schema>.
    Validate the <data> against the <schema>.
    Return a <Valid: status>.
}

Native Plugins

For performance-critical operations or system integrations, you can write native plugins in Swift, Rust, C, or Python.

Swift Plugins

Use the AROPluginKit SDK with the @AROExport macro:

// Sources/CSVParser.swift
import AROPluginKit

@AROExport
private let plugin = AROPlugin(name: "csv-parser", version: "1.0.0", handle: "CSV")
    .action("ParseCSV", verbs: ["parsecsv"], role: "own",
            prepositions: ["from"],
            description: "Parse CSV data into rows") { input in
        let data = input.string("data")
            ?? input.from.string("data")
            ?? ""
        let rows = parseCSV(data)
        return .success(["rows": rows, "count": rows.count])
    }

Rust Plugins

Use #[action] and aro_export! macros from the SDK:

// src/lib.rs
use aro_plugin_sdk::prelude::*;

#[action(name = "ParseCSV", verbs = ["parsecsv"], role = "own",
         prepositions = ["from"], description = "Parse CSV data")]
fn parse_csv(input: &Input) -> PluginResult<Output> {
    let data = input.string("data")
        .ok_or_else(|| PluginError::missing("data"))?;
    let rows = parse(data);
    Ok(Output::new().set("rows", json!(rows)))
}

aro_export! {
    name: "csv-parser-rs",
    version: "1.0.0",
    handle: "CSV",
    actions: [parse_csv],
    qualifiers: [],
}

Python Plugins

Use @plugin and @action decorators:

# src/plugin.py
from aro_plugin_sdk import plugin, action, export_abi, run, AROInput

@plugin(name="csv-tools-py", version="1.0.0", handle="CSV")
class CSVPlugin:
    pass

@action(name="parse-csv", verbs=["parsecsv"], role="own",
        prepositions=["from"], description="Parse CSV data")
def handle_parse_csv(input: AROInput):
    content = input.string("data")
    rows = parse_csv(content)
    return {"rows": rows, "count": len(rows)}

export_abi(globals())

if __name__ == "__main__":
    run()

C Plugins

Use ARO_PLUGIN() and ARO_ACTION() macros from the single-header SDK:

/* src/csv_plugin.c */
#define ARO_PLUGIN_SDK_IMPLEMENTATION
#include "aro_plugin_sdk.h"

ARO_PLUGIN("csv-parser-c", "1.0.0")
ARO_HANDLE("CSV")

/* Lifecycle hooks (called on load/unload) */
ARO_INIT() {
    /* initialise resources */
}

ARO_SHUTDOWN() {
    /* clean up resources */
}

ARO_ACTION("ParseCSV", "own", "from,with") {
    const char* data = aro_input_string(ctx, "data");
    /* parse CSV and populate output */
    aro_output_string(ctx, "rows", result);
    aro_output_int(ctx, "count", row_count);
    return aro_ok(ctx);
}

Plugin SDK Registration

Each language has an idiomatic SDK that generates the required C ABI exports automatically:

LanguageRegistrationSDK
Swift@AROExport macroAROPluginKit (SPM)
Rust#[action] + aro_export!aro-plugin-sdk (crate)
C / C++ARO_PLUGIN() + ARO_ACTION()aro_plugin_sdk.h (header)
Python@plugin + @action + export_abi()aro-plugin-sdk (pip)

Plugin Qualifiers

Plugins can register custom qualifiers that transform values. Qualifiers are namespaced via the plugin's handle and accessed as handle.qualifier in ARO code:

(* Using qualifiers from a plugin with handle: Collections *)
Compute the <random-item: Collections.pick-random> from the <items>.
Compute the <shuffled: Collections.shuffle> from the <items>.
Log <items: Collections.reverse> to the <console>.

Swift Qualifiers

@AROExport
private let plugin = AROPlugin(name: "my-plugin", version: "1.0.0", handle: "MyPlugin")
    .qualifier("reverse", inputTypes: ["List", "String"],
               description: "Reverse elements or characters") { params in
        if let array = params.arrayValue {
            return .success(Array(array.reversed()))
        }
        if let string = params.stringValue {
            return .success(String(string.reversed()))
        }
        return .failure("reverse requires a list or string")
    }

C Qualifiers

ARO_QUALIFIER("first", "List", "Returns the first element") {
    aro_array* arr = aro_qualifier_array(ctx);
    if (!arr || aro_array_length(arr) == 0)
        return aro_error(ctx, ARO_ERR_INVALID_INPUT,
                         "first requires a non-empty list");
    const char* elem = aro_array_string(arr, 0);
    return aro_qualifier_result_string(ctx, elem);
}

Rust Qualifiers

#[qualifier(name = "reverse", input_types = ["List", "String"])]
fn reverse(input: &QualifierInput) -> PluginResult<QualifierOutput> {
    // ...
}

aro_export! {
    name: "my-plugin",
    version: "1.0.0",
    handle: "MyPlugin",
    actions: [],
    qualifiers: [reverse],
}

Python Qualifiers

from aro_plugin_sdk import plugin, qualifier, export_abi, run

@plugin(name="my-plugin", version="1.0.0", handle="MyPlugin")
class MyPlugin:
    pass

@qualifier(name="sort", description="Sort a list")
def handle_sort(value, params=None):
    if isinstance(value, list):
        return sorted(value)
    return value

export_abi(globals())

C ABI Interface

All plugin SDKs generate a common C ABI. You don't need to implement these manually when using an SDK, but here is the interface for reference:

SymbolRequiredDescription
aro_plugin_info()YesReturns plugin metadata as JSON
aro_plugin_free(ptr)YesFrees memory allocated by the plugin
aro_plugin_execute(action, input)NoDispatches an action by name
aro_plugin_qualifier(name, input)NoDispatches a qualifier by name
aro_plugin_init()NoCalled when the plugin is loaded
aro_plugin_shutdown()NoCalled when the plugin is unloaded

Testing Your Package

Create test files alongside your feature sets:

(* tests/csv-parser.test.aro *)

(Test Parse CSV: CSV Parser Tests) {
    Create the <test-data> with "name,age\nAlice,30\nBob,25".
    ParseCSV the <result> from the <test-data>.
    Assert that <result: length> equals 2.
    Assert that <result: 0 name> equals "Alice".
    Return a <Passed: status>.
}

Run tests with:

aro test ./my-package

Publishing Your Package

ARO uses Git repositories for package distribution. To share your package:

  1. Create a Git repository for your package
  2. Ensure plugin.yaml is in the root
  3. Tag releases with semantic versions (e.g., v1.0.0)
  4. Share the Git URL with users
# Users can install with:
aro add git@github.com:yourname/csv-tools.git
aro add git@github.com:yourname/csv-tools.git --ref v1.0.0

Version Compatibility

Always specify the aro-version field in your plugin.yaml to indicate which ARO versions your package supports. Use semantic version ranges like >=0.2.0 or ^1.0.0.

Best Practices

Related Documentation

Packages - Installing and using packages
Custom Actions - The ActionImplementation protocol
Feature Sets - Organizing code into feature sets