1use 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#[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 MapEntry(Vec<ImmutableValue>),
45 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 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
111pub type TaskId = u64;
113
114#[derive(Clone, Debug, PartialEq)]
116pub enum TaskEvent {
117 Pending,
118 Resolved(Value),
119 Rejected(Error),
120}
121
122pub 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}