Skip to content

Commit d8ffb44

Browse files
Merge pull request #280 from code0-tech/#277-native-definitions
native definitions
2 parents 179553c + 851501c commit d8ffb44

28 files changed

Lines changed: 3793 additions & 258 deletions

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = [ "crates/taurus-core", "crates/taurus-manual", "crates/taurus", "crates/taurus-tests", "crates/taurus-provider", "crates/taurus-bench"]
2+
members = [ "crates/taurus-core", "crates/taurus-macros", "crates/taurus-manual", "crates/taurus", "crates/taurus-tests", "crates/taurus-provider", "crates/taurus-bench"]
33
resolver = "3"
44

55
[workspace.package]
@@ -27,13 +27,20 @@ serde = "1.0.228"
2727
uuid = { version = "1.23.0", features = ["v4"] }
2828
ureq = "3.0.0"
2929
chrono = { version = "0.4.42", default-features = false, features = ["std", "clock"] }
30+
inventory = "0.3.24"
31+
syn = { version = "2", features = ["full", "extra-traits"] }
32+
quote = "1"
33+
proc-macro2 = "1"
3034

3135
[workspace.dependencies.taurus-core]
3236
path = "./crates/taurus-core"
3337

3438
[workspace.dependencies.taurus-provider]
3539
path = "./crates/taurus-provider"
3640

41+
[workspace.dependencies.taurus-macros]
42+
path = "./crates/taurus-macros"
43+
3744
# `cargo build --profile profiling` — a release build with debug symbols,
3845
# for flamegraph/sampling work. Kept separate from [profile.release] so
3946
# ordinary release builds stay free of the extra symbol/binary-size cost.

crates/taurus-core/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ serde_json = { workspace = true }
1717
ureq = { workspace = true }
1818
tokio = { workspace = true }
1919
chrono = { workspace = true }
20+
inventory = { workspace = true }
21+
taurus-macros = { workspace = true }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//! Exports every self-registered module (see `taurus_core::registry::build_modules`)
2+
//! as a directory of JSON files, mirroring the old `definitions/*.json` layout.
3+
//! Useful for eyeballing/diffing what Taurus would push to Aquila without a live
4+
//! Aquila instance.
5+
//!
6+
//! Usage: `cargo run -p taurus-core --example export_definitions [output-dir]`
7+
//! (defaults to `./export-definitions`).
8+
9+
fn main() {
10+
let output_dir = std::env::args()
11+
.nth(1)
12+
.unwrap_or_else(|| "./export-definitions".to_string());
13+
let output_dir = std::path::Path::new(&output_dir);
14+
15+
let modules = taurus_core::registry::build_modules();
16+
taurus_core::export::write_all(&modules, output_dir).expect("failed to export definitions");
17+
18+
println!(
19+
"Exported {} module(s) to {}",
20+
modules.len(),
21+
output_dir.display()
22+
);
23+
}

crates/taurus-core/src/export.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//! Renders [`build_modules`](crate::registry::build_modules)'s output as a
2+
//! directory tree of JSON files -- one directory per module, containing
3+
//! `module.json` plus one file per data type / function / runtime function --
4+
//! using the same field names and camelCase wire format Aquila accepts,
5+
//! since `tucana::shared`'s `Serialize` impls already implement protobuf's
6+
//! canonical JSON mapping. Mirrors the layout the old `definitions/*.json`
7+
//! tree used, so this is primarily a debugging/parity-check tool: run it and
8+
//! diff the result against a known-good definitions dump.
9+
10+
use std::fs;
11+
use std::io;
12+
use std::path::Path;
13+
14+
use serde::Serialize;
15+
use tucana::shared::{Module, Translation};
16+
17+
/// Writes one subdirectory per module under `root`, named by the module's
18+
/// identifier.
19+
pub fn write_all(modules: &[Module], root: &Path) -> io::Result<()> {
20+
for module in modules {
21+
write(module, &root.join(&module.identifier))?;
22+
}
23+
Ok(())
24+
}
25+
26+
fn write(module: &Module, dir: &Path) -> io::Result<()> {
27+
fs::create_dir_all(dir)?;
28+
write_json(&dir.join("module.json"), &Meta::from(module))?;
29+
30+
write_each(
31+
&dir.join("data_types"),
32+
&module.definition_data_types,
33+
|dt| &dt.identifier,
34+
)?;
35+
write_each(&dir.join("functions"), &module.function_definitions, |f| {
36+
&f.runtime_name
37+
})?;
38+
write_each(
39+
&dir.join("runtime_functions"),
40+
&module.runtime_function_definitions,
41+
|f| &f.runtime_name,
42+
)?;
43+
44+
Ok(())
45+
}
46+
47+
#[derive(Serialize)]
48+
struct Meta<'a> {
49+
identifier: &'a str,
50+
name: &'a [Translation],
51+
description: &'a [Translation],
52+
documentation: &'a str,
53+
author: &'a str,
54+
icon: &'a str,
55+
version: &'a str,
56+
}
57+
58+
impl<'a> From<&'a Module> for Meta<'a> {
59+
fn from(module: &'a Module) -> Self {
60+
Self {
61+
identifier: &module.identifier,
62+
name: &module.name,
63+
description: &module.description,
64+
documentation: &module.documentation,
65+
author: &module.author,
66+
icon: &module.icon,
67+
version: &module.version,
68+
}
69+
}
70+
}
71+
72+
fn write_each<T: Serialize>(
73+
dir: &Path,
74+
items: &[T],
75+
identifier: impl Fn(&T) -> &str,
76+
) -> io::Result<()> {
77+
if items.is_empty() {
78+
return Ok(());
79+
}
80+
fs::create_dir_all(dir)?;
81+
for item in items {
82+
let file_name = identifier(item).replace("::", "_");
83+
write_json(&dir.join(format!("{file_name}.json")), item)?;
84+
}
85+
Ok(())
86+
}
87+
88+
fn write_json<T: Serialize>(path: &Path, value: &T) -> io::Result<()> {
89+
let json = serde_json::to_string_pretty(value)
90+
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
91+
fs::write(path, json)
92+
}
93+
94+
#[cfg(test)]
95+
mod tests {
96+
use tucana::shared::{DefinitionDataType, Module, Translation};
97+
98+
use super::write_all;
99+
100+
fn sample_modules() -> Vec<Module> {
101+
vec![Module {
102+
identifier: "example-module".into(),
103+
version: "0.1.0".into(),
104+
author: "code0-tech".into(),
105+
icon: "tabler:bolt".into(),
106+
documentation: "An example".into(),
107+
name: vec![Translation {
108+
code: "en-US".into(),
109+
content: "Example".into(),
110+
}],
111+
definition_data_types: vec![DefinitionDataType {
112+
identifier: "EMAIL".into(),
113+
r#type: "string".into(),
114+
version: "0.1.0".into(),
115+
..Default::default()
116+
}],
117+
..Default::default()
118+
}]
119+
}
120+
121+
#[test]
122+
fn writes_one_directory_per_module_with_one_file_per_definition() {
123+
let dir = std::env::temp_dir().join(format!("taurus-export-test-{}", std::process::id()));
124+
write_all(&sample_modules(), &dir).expect("export succeeds");
125+
126+
let meta: serde_json::Value = serde_json::from_slice(
127+
&std::fs::read(dir.join("example-module").join("module.json")).unwrap(),
128+
)
129+
.unwrap();
130+
assert_eq!(meta["identifier"], "example-module");
131+
132+
let email: serde_json::Value = serde_json::from_slice(
133+
&std::fs::read(
134+
dir.join("example-module")
135+
.join("data_types")
136+
.join("EMAIL.json"),
137+
)
138+
.unwrap(),
139+
)
140+
.unwrap();
141+
assert_eq!(email["identifier"], "EMAIL");
142+
143+
// No functions were registered, so that directory shouldn't exist.
144+
assert!(!dir.join("example-module").join("functions").exists());
145+
146+
std::fs::remove_dir_all(&dir).ok();
147+
}
148+
}

crates/taurus-core/src/handler/registry.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
33
use crate::handler::argument::{Argument, ParameterNode, Thunk};
44
use crate::runtime::execution::value_store::ValueStore;
5-
use crate::runtime::functions::ALL_FUNCTION_SETS;
65
use crate::types::signal::Signal;
76
use std::collections::HashMap;
87

@@ -69,6 +68,10 @@ pub struct FunctionRegistration {
6968
pub entry: HandlerFunctionEntry,
7069
}
7170

71+
// Populated by `#[taurus_macros::runtime_function(...)]` via
72+
// `inventory::submit!`.
73+
inventory::collect!(FunctionRegistration);
74+
7275
impl FunctionRegistration {
7376
pub const fn eager(id: &'static str, handler: HandlerFn, param_count: u8) -> Self {
7477
Self {
@@ -97,8 +100,8 @@ pub struct FunctionStore {
97100
impl Default for FunctionStore {
98101
fn default() -> Self {
99102
let mut store = Self::new();
100-
for set in ALL_FUNCTION_SETS {
101-
store.populate(set);
103+
for reg in inventory::iter::<FunctionRegistration>() {
104+
store.functions.insert(reg.id, reg.entry);
102105
}
103106
store
104107
}
@@ -117,7 +120,9 @@ impl FunctionStore {
117120
self.functions.get(id)
118121
}
119122

120-
/// Register a group of handlers.
123+
/// Register a group of handlers. Only used by tests to inject
124+
/// test-only handlers without polluting the global inventory registry.
125+
#[cfg(test)]
121126
pub fn populate(&mut self, regs: &[FunctionRegistration]) {
122127
for reg in regs {
123128
self.functions.insert(reg.id, reg.entry);

crates/taurus-core/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@
1313
//! JSON fixture and proto-value-normalization helpers shared by the runtime
1414
//! binaries.
1515
16+
pub mod export;
1617
pub mod fixtures;
1718
mod handler;
19+
pub mod meta;
1820
pub mod normalize;
21+
pub mod registry;
1922
pub mod runtime;
2023
pub mod time;
2124
pub mod types;

crates/taurus-core/src/meta.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//! Metadata that used to live in `definitions/*.json`, now declared inline
2+
//! via `#[taurus_macros::runtime_function]` / `taurus_macros::data_type!` /
3+
//! `taurus_macros::module!` next to the code it describes, and collected at
4+
//! startup through `inventory` instead of being read from disk. See
5+
//! [`crate::registry::build_modules`] for how this gets turned into
6+
//! `tucana::shared::Module`s.
7+
8+
use tucana::shared::{DefinitionDataTypeRule, Translation};
9+
10+
pub struct ParameterMeta {
11+
pub runtime_name: &'static str,
12+
pub name: Vec<Translation>,
13+
pub description: Vec<Translation>,
14+
pub documentation: Vec<Translation>,
15+
}
16+
17+
pub struct RuntimeFunctionMeta {
18+
pub identifier: &'static str,
19+
/// Identifier of the [`ModuleMeta`] this function belongs to.
20+
pub module: &'static str,
21+
pub signature: &'static str,
22+
pub name: Vec<Translation>,
23+
pub description: Vec<Translation>,
24+
pub documentation: Vec<Translation>,
25+
pub display_message: Vec<Translation>,
26+
pub alias: Vec<Translation>,
27+
pub display_icon: Option<&'static str>,
28+
pub throws_error: bool,
29+
pub parameters: Vec<ParameterMeta>,
30+
pub linked_data_type_identifiers: Vec<&'static str>,
31+
}
32+
33+
pub struct DataTypeMeta {
34+
pub identifier: &'static str,
35+
/// Identifier of the [`ModuleMeta`] this data type belongs to.
36+
pub module: &'static str,
37+
pub name: Vec<Translation>,
38+
pub display_message: Vec<Translation>,
39+
pub alias: Vec<Translation>,
40+
pub generic_keys: Vec<&'static str>,
41+
/// Hand-authored structural type string (e.g. `"boolean"`, or a
42+
/// TypeScript-like conditional type for generic data types) -- data
43+
/// types have no Rust type to derive this from.
44+
pub type_string: &'static str,
45+
pub linked_data_type_identifiers: Vec<&'static str>,
46+
pub rules: Vec<DefinitionDataTypeRule>,
47+
}
48+
49+
pub struct ModuleMeta {
50+
pub identifier: &'static str,
51+
pub name: Vec<Translation>,
52+
pub description: Vec<Translation>,
53+
pub documentation: &'static str,
54+
pub author: &'static str,
55+
pub icon: &'static str,
56+
pub version: &'static str,
57+
}
58+
59+
pub struct MetaRegistration(pub fn() -> RuntimeFunctionMeta);
60+
inventory::collect!(MetaRegistration);
61+
62+
pub struct DataTypeRegistration(pub fn() -> DataTypeMeta);
63+
inventory::collect!(DataTypeRegistration);
64+
65+
pub struct ModuleRegistration(pub fn() -> ModuleMeta);
66+
inventory::collect!(ModuleRegistration);

0 commit comments

Comments
 (0)