Skip to main content

hara_abi/
lib.rs

1//! Dependency-free values and identities shared across Hara ABI boundaries.
2
3use std::collections::BTreeMap;
4use std::time::Duration;
5
6pub const HTA_V1: &str = "hta.v1";
7
8#[derive(Clone, Debug, PartialEq)]
9pub enum Value {
10    Nil,
11    Boolean(bool),
12    String(String),
13    Integer(i64),
14    BigInteger(String),
15    Float(f64),
16    Bytes(Vec<u8>),
17    Keyword(String),
18    Vector(Vec<Value>),
19    Record(RecordValue),
20}
21
22pub type RecordValue = BTreeMap<String, Value>;
23
24/// The complete immutable Hara data profile used by HTA and durable stores.
25///
26/// This is intentionally separate from [`Value`], whose closed shape is the
27/// stable ABI accepted by existing native provider crates.
28#[derive(Clone, Debug, PartialEq)]
29pub enum ImmutableValue {
30    Nil,
31    Boolean(bool),
32    String(String),
33    Integer(i64),
34    Float(f64),
35    Character(char),
36    BigInteger(String),
37    Regex(String),
38    Bytes(Vec<u8>),
39    Keyword(String),
40    Symbol(String),
41    List(Vec<ImmutableValue>),
42    Vector(Vec<ImmutableValue>),
43    /// A dedicated two-value entry produced by map and lookup operations.
44    MapEntry(Vec<ImmutableValue>),
45    /// Legacy compact tuple representation retained for decoding old HTA data.
46    Tuple(Vec<ImmutableValue>),
47    Cons(Vec<ImmutableValue>),
48    Queue(Vec<ImmutableValue>),
49    Set(Vec<ImmutableValue>),
50    OrderedSet(Vec<ImmutableValue>),
51    SortedSet(Vec<ImmutableValue>),
52    Map(Vec<(ImmutableValue, ImmutableValue)>),
53    OrderedMap(Vec<(ImmutableValue, ImmutableValue)>),
54    SortedMap(Vec<(ImmutableValue, ImmutableValue)>),
55    Trie(Vec<(String, ImmutableValue)>),
56    Record(ImmutableRecordValue),
57    Tagged {
58        tag: String,
59        form: Box<ImmutableValue>,
60    },
61    ExceptionInfo {
62        message: String,
63        data: Box<ImmutableValue>,
64        cause: Option<Box<ImmutableValue>>,
65        provenance: ExceptionProvenance,
66    },
67    Struct {
68        name: String,
69        fields: Vec<String>,
70        values: Vec<ImmutableValue>,
71    },
72    Pointer {
73        context: String,
74        fields: ImmutableRecordValue,
75    },
76    /// A qualified binding identity. The bound value is never transferred.
77    VarRef(String),
78}
79
80pub type ImmutableRecordValue = BTreeMap<String, ImmutableValue>;
81
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub struct ExceptionSite {
84    pub namespace: Option<String>,
85    pub resource: Option<String>,
86    pub line: u64,
87    pub column: u64,
88}
89
90#[derive(Clone, Debug, PartialEq, Eq, Default)]
91pub struct ExceptionProvenance {
92    pub created_at: Option<ExceptionSite>,
93    pub throws: Vec<ExceptionSite>,
94}
95
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub struct Error {
98    pub code: String,
99    pub detail: String,
100}
101
102impl Error {
103    pub fn new(code: impl Into<String>, detail: impl Into<String>) -> Self {
104        Self {
105            code: code.into(),
106            detail: detail.into(),
107        }
108    }
109}
110
111/// Opaque task identifier owned by one linked native module.
112pub type TaskId = u64;
113
114/// Observable state of an asynchronous native module call.
115#[derive(Clone, Debug, PartialEq)]
116pub enum TaskEvent {
117    Pending,
118    Resolved(Value),
119    Rejected(Error),
120}
121
122/// Dependency-free contract implemented by publication-linked native crates.
123pub trait NativeModule: Send + Sync {
124    fn identity(&self) -> &NativeIdentity;
125    fn operations(&self) -> &[&str];
126    fn capabilities(&self) -> &[&str];
127    fn start(&self, operation: &str, arguments: Vec<Value>) -> Result<TaskId, Error>;
128    fn poll(&self, task: TaskId) -> Result<TaskEvent, Error>;
129
130    fn wait(&self, task: TaskId, timeout: Option<Duration>) -> Result<TaskEvent, Error> {
131        let _ = timeout;
132        self.poll(task)
133    }
134
135    fn cancel(&self, task: TaskId) -> Result<(), Error>;
136    fn drop_task(&self, task: TaskId);
137    fn shutdown(&self);
138}
139
140#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
141pub struct NativeIdentity {
142    pub package: String,
143    pub export: String,
144    pub crate_name: String,
145    pub abi: String,
146}
147
148impl NativeIdentity {
149    pub fn new(
150        package: impl Into<String>,
151        export: impl Into<String>,
152        crate_name: impl Into<String>,
153        abi: impl Into<String>,
154    ) -> Result<Self, Error> {
155        let identity = Self {
156            package: package.into(),
157            export: export.into(),
158            crate_name: crate_name.into(),
159            abi: abi.into(),
160        };
161        for (label, value) in [
162            ("package", identity.package.as_str()),
163            ("export", identity.export.as_str()),
164            ("crate", identity.crate_name.as_str()),
165            ("abi", identity.abi.as_str()),
166        ] {
167            if value.is_empty() || value.chars().any(char::is_whitespace) {
168                return Err(Error::new(
169                    "native-identity-invalid",
170                    format!("{label} must be a non-empty identifier"),
171                ));
172            }
173        }
174        Ok(identity)
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn native_identities_are_exact_and_portable() {
184        let identity = NativeIdentity::new(
185            "gh:greenways-ai:hoplite-store-sqlite",
186            "hoplite/store",
187            "hoplite-store-sqlite",
188            "hoplite-auth-store/0-alpha",
189        )
190        .unwrap();
191        assert_eq!(identity.crate_name, "hoplite-store-sqlite");
192        assert_eq!(
193            NativeIdentity::new("", "hoplite/store", "crate", "abi")
194                .unwrap_err()
195                .code,
196            "native-identity-invalid"
197        );
198    }
199
200    #[test]
201    fn abi_values_cover_portable_database_payloads() {
202        let value = Value::Record(BTreeMap::from([
203            ("ok".into(), Value::Boolean(true)),
204            (
205                "rows".into(),
206                Value::Vector(vec![Value::Vector(vec![Value::Integer(1), Value::Nil])]),
207            ),
208            (
209                "big".into(),
210                Value::BigInteger("9223372036854775808".into()),
211            ),
212            ("numeric".into(), Value::Float(12.50)),
213        ]));
214        assert!(matches!(value, Value::Record(_)));
215    }
216}