Repository navigation
Expand file tree
/
Copy pathmanifest.rs
More file actions
145 lines (133 loc) · 4.55 KB
/
Copy pathmanifest.rs
File metadata and controls
145 lines (133 loc) · 4.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! The registration manifest: the single source of truth for which dotted
//! capability names exist, and the optional TypeScript signature for each.
//! Both the runtime `php.*` facade and the generated `.d.ts` are derived from
//! this same list, so types and runtime can never drift.
/// One registered capability.
#[derive(Debug, Clone)]
pub struct ManifestEntry {
/// Flat, dotted name, e.g. `"db.query"`.
pub name: String,
/// Optional TypeScript signature, e.g. `"query(sql: string): Row[]"`.
pub types: Option<String>,
}
/// A node in the nested namespace tree built from dotted names.
#[derive(Default)]
struct Node {
children: Vec<(String, Node)>,
/// If this node is a leaf (a registered function), its optional signature.
leaf: Option<Option<String>>,
}
impl Node {
fn child_mut(&mut self, key: &str) -> &mut Node {
if let Some(idx) = self.children.iter().position(|(k, _)| k == key) {
return &mut self.children[idx].1;
}
self.children.push((key.to_owned(), Node::default()));
&mut self.children.last_mut().unwrap().1
}
}
fn build_tree(entries: &[ManifestEntry]) -> Node {
let mut root = Node::default();
for e in entries {
let mut node = &mut root;
let parts: Vec<&str> = e.name.split('.').collect();
for part in &parts[..parts.len() - 1] {
node = node.child_mut(part);
}
node.child_mut(parts[parts.len() - 1]).leaf = Some(e.types.clone());
}
root
}
/// Generate a `.d.ts` declaration for the `php` global from the manifest.
pub fn to_dts(entries: &[ManifestEntry]) -> String {
let tree = build_tree(entries);
let mut out = String::from("// Generated by php-quickjs. Do not edit by hand.\n");
out.push_str("declare const php: ");
write_node(&tree, 0, &mut out);
out.push_str(";\n");
out
}
fn indent(level: usize, out: &mut String) {
for _ in 0..level {
out.push_str(" ");
}
}
fn write_node(node: &Node, level: usize, out: &mut String) {
out.push_str("{\n");
for (key, child) in &node.children {
indent(level + 1, out);
let member = if is_ident(key) {
key.clone()
} else {
format!("\"{key}\"")
};
match &child.leaf {
Some(sig) => {
// A registered function leaf.
match sig {
Some(s) if s.contains('(') => {
// A full signature like `query(sql: string): Row[]`.
// Re-key it onto the member name if it starts with the
// leaf identifier, else emit as-is.
out.push_str(&normalize_sig(key, s));
}
Some(s) => {
out.push_str(&format!("{member}: {s}"));
}
None => {
out.push_str(&format!("{member}(...args: any[]): any"));
}
}
out.push_str(";\n");
}
None => {
out.push_str(&format!("{member}: "));
write_node(child, level + 1, out);
out.push_str(";\n");
}
}
}
indent(level, out);
out.push('}');
}
/// If the signature already begins with the leaf name (e.g.
/// `query(sql: string): Row[]`), use it verbatim; otherwise prefix the name.
fn normalize_sig(name: &str, sig: &str) -> String {
let trimmed = sig.trim();
if trimmed.starts_with(name) {
trimmed.to_owned()
} else {
format!("{name}: {trimmed}")
}
}
fn is_ident(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' || c == '$' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
}
#[cfg(test)]
mod tests {
use super::*;
fn e(name: &str, types: Option<&str>) -> ManifestEntry {
ManifestEntry {
name: name.to_owned(),
types: types.map(str::to_owned),
}
}
#[test]
fn nested_dts() {
let entries = vec![
e("db.query", Some("query(sql: string): unknown[]")),
e("db.execute", None),
e("log.info", Some("info(msg: string): void")),
];
let dts = to_dts(&entries);
assert!(dts.contains("db: {"));
assert!(dts.contains("query(sql: string): unknown[];"));
assert!(dts.contains("execute(...args: any[]): any;"));
assert!(dts.contains("info(msg: string): void;"));
}
}