mirror of
https://github.com/kennethnym/aris.git
synced 2026-03-20 00:51:20 +00:00
feat: add actions to FeedSource interface
Add listActions() and executeAction() to FeedSource for write operations back to external services. Actions use arktype schemas for input validation via StandardSchemaV1. - ActionDefinition type with optional input schema - FeedEngine routes actions with existence and ID validation - Source IDs use reverse-domain format (aris.location, aris.tfl) - LocationSource: update-location action with schema validation - TflSource: set-lines-of-interest action with lineId validation - No-op implementations for sources without actions Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
"test": "bun test src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aris/core": "workspace:*"
|
||||
"@aris/core": "workspace:*",
|
||||
"arktype": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
export {
|
||||
LocationSource,
|
||||
LocationKey,
|
||||
type Location,
|
||||
type LocationSourceOptions,
|
||||
} from "./location-source.ts"
|
||||
export { LocationSource, LocationKey } from "./location-source.ts"
|
||||
export { Location, type LocationSourceOptions } from "./types.ts"
|
||||
|
||||
@@ -16,7 +16,7 @@ describe("LocationSource", () => {
|
||||
describe("FeedSource interface", () => {
|
||||
test("has correct id", () => {
|
||||
const source = new LocationSource()
|
||||
expect(source.id).toBe("location")
|
||||
expect(source.id).toBe("aris.location")
|
||||
})
|
||||
|
||||
test("fetchItems always returns empty array", async () => {
|
||||
@@ -147,4 +147,40 @@ describe("LocationSource", () => {
|
||||
expect(listener2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("actions", () => {
|
||||
test("listActions returns update-location action", async () => {
|
||||
const source = new LocationSource()
|
||||
const actions = await source.listActions()
|
||||
|
||||
expect(actions["update-location"]).toBeDefined()
|
||||
expect(actions["update-location"]!.id).toBe("update-location")
|
||||
expect(actions["update-location"]!.input).toBeDefined()
|
||||
})
|
||||
|
||||
test("executeAction update-location pushes location", async () => {
|
||||
const source = new LocationSource()
|
||||
|
||||
expect(source.lastLocation).toBeNull()
|
||||
|
||||
const location = createLocation({ lat: 40.7128, lng: -74.006 })
|
||||
await source.executeAction("update-location", location)
|
||||
|
||||
expect(source.lastLocation).toEqual(location)
|
||||
})
|
||||
|
||||
test("executeAction throws on invalid input", async () => {
|
||||
const source = new LocationSource()
|
||||
|
||||
await expect(
|
||||
source.executeAction("update-location", { lat: "not a number" }),
|
||||
).rejects.toThrow()
|
||||
})
|
||||
|
||||
test("executeAction throws for unknown action", async () => {
|
||||
const source = new LocationSource()
|
||||
|
||||
await expect(source.executeAction("nonexistent", {})).rejects.toThrow("Unknown action")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
import type { Context, FeedSource } from "@aris/core"
|
||||
import type { ActionDefinition, Context, FeedSource } from "@aris/core"
|
||||
|
||||
import { contextKey, type ContextKey } from "@aris/core"
|
||||
import { UnknownActionError, contextKey, type ContextKey } from "@aris/core"
|
||||
import { type } from "arktype"
|
||||
|
||||
/**
|
||||
* Geographic coordinates with accuracy and timestamp.
|
||||
*/
|
||||
export interface Location {
|
||||
lat: number
|
||||
lng: number
|
||||
/** Accuracy in meters */
|
||||
accuracy: number
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
export interface LocationSourceOptions {
|
||||
/** Number of locations to retain in history. Defaults to 1. */
|
||||
historySize?: number
|
||||
}
|
||||
import { Location, type LocationSourceOptions } from "./types.ts"
|
||||
|
||||
export const LocationKey: ContextKey<Location> = contextKey("location")
|
||||
|
||||
@@ -29,7 +16,7 @@ export const LocationKey: ContextKey<Location> = contextKey("location")
|
||||
* Does not produce feed items - always returns empty array from `fetchItems`.
|
||||
*/
|
||||
export class LocationSource implements FeedSource {
|
||||
readonly id = "location"
|
||||
readonly id = "aris.location"
|
||||
|
||||
private readonly historySize: number
|
||||
private locations: Location[] = []
|
||||
@@ -39,6 +26,31 @@ export class LocationSource implements FeedSource {
|
||||
this.historySize = options.historySize ?? 1
|
||||
}
|
||||
|
||||
async listActions(): Promise<Record<string, ActionDefinition>> {
|
||||
return {
|
||||
"update-location": {
|
||||
id: "update-location",
|
||||
description: "Push a new location update",
|
||||
input: Location,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async executeAction(actionId: string, params: unknown): Promise<void> {
|
||||
switch (actionId) {
|
||||
case "update-location": {
|
||||
const result = Location(params)
|
||||
if (result instanceof type.errors) {
|
||||
throw new Error(result.summary)
|
||||
}
|
||||
this.pushLocation(result)
|
||||
return
|
||||
}
|
||||
default:
|
||||
throw new UnknownActionError(actionId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a new location update. Notifies all context listeners.
|
||||
*/
|
||||
|
||||
17
packages/aris-source-location/src/types.ts
Normal file
17
packages/aris-source-location/src/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { type } from "arktype"
|
||||
|
||||
/** Geographic coordinates with accuracy and timestamp. */
|
||||
export const Location = type({
|
||||
lat: "number",
|
||||
lng: "number",
|
||||
/** Accuracy in meters */
|
||||
accuracy: "number",
|
||||
timestamp: "Date",
|
||||
})
|
||||
|
||||
export type Location = typeof Location.infer
|
||||
|
||||
export interface LocationSourceOptions {
|
||||
/** Number of locations to retain in history. Defaults to 1. */
|
||||
historySize?: number
|
||||
}
|
||||
Reference in New Issue
Block a user