1use std::path::PathBuf;
25
26use lemon::meta;
27use serde_json::{json, Map, Value};
28
29fn field_schema(kind: &str) -> Value {
31 match kind {
32 "expr" => json!({ "$ref": "#/$defs/Expr" }),
33 "number" => json!({ "type": "number" }),
34 "bool" => json!({ "type": "boolean" }),
35 "string" => json!({ "type": "string" }),
36 "list" => json!({ "type": "array", "items": { "$ref": "#/$defs/Expr" } }),
38 "list-string" => json!({ "type": "array", "items": { "type": "string" } }),
39 other => panic!("unknown field kind `{other}`"),
40 }
41}
42
43fn build_catalog() -> Value {
45 let mut ops: Vec<Value> = Vec::new();
46
47 ops.push(json!({
49 "name": "Data",
50 "tag": "Data",
51 "form": "leaf",
52 "description": "A raw input series by name (e.g. close, pe, revenue_growth). In DSL, a bare identifier lowers to this.",
53 "args": [
54 { "name": "name", "kind": "string", "required": true }
55 ]
56 }));
57 ops.push(json!({
58 "name": "Const",
59 "tag": "Const",
60 "form": "leaf",
61 "description": "A constant scalar value, broadcast across the panel. In DSL, a bare number operand lowers to this.",
62 "args": [
63 { "name": "value", "kind": "number", "required": true }
64 ]
65 }));
66
67 for op in meta::function_ops() {
69 let args: Vec<Value> = op
70 .fields
71 .iter()
72 .map(|f| {
73 let mut m = Map::new();
74 m.insert("name".into(), json!(f.name));
75 m.insert("kind".into(), json!(f.kind));
76 m.insert("required".into(), json!(f.required));
77 if let Some(d) = &f.default {
78 m.insert("default".into(), d.clone());
79 }
80 Value::Object(m)
81 })
82 .collect();
83 ops.push(json!({
84 "name": op.name,
85 "aliases": op.aliases,
86 "tag": op.tag,
87 "form": "function",
88 "description": op.description,
89 "args": args,
90 }));
91 }
92
93 for (symbol, tag) in meta::binary_operators() {
95 ops.push(json!({
96 "name": symbol,
97 "tag": tag,
98 "form": "operator",
99 "description": binop_description(tag),
100 "args": [
101 { "name": "l", "kind": "expr", "required": true },
102 { "name": "r", "kind": "expr", "required": true }
103 ]
104 }));
105 }
106
107 ops.push(json!({
109 "name": "-",
110 "tag": "Neg",
111 "form": "operator",
112 "unary": true,
113 "description": "Negation (-`of`).",
114 "args": [
115 { "name": "of", "kind": "expr", "required": true }
116 ]
117 }));
118 ops.push(json!({
119 "name": "not",
120 "tag": "Not",
121 "form": "operator",
122 "unary": true,
123 "description": "Logical NOT of a boolean panel (NaN is falsy, so `not` of NaN is 1).",
124 "args": [
125 { "name": "of", "kind": "expr", "required": true }
126 ]
127 }));
128
129 Value::Array(ops)
130}
131
132fn binop_description(tag: &str) -> &'static str {
134 match tag {
135 "Gt" => "1 where `l` is greater than `r`, else 0.",
136 "Lt" => "1 where `l` is less than `r`, else 0.",
137 "Ge" => "1 where `l` is greater than or equal to `r`, else 0.",
138 "Le" => "1 where `l` is less than or equal to `r`, else 0.",
139 "And" => "Logical AND of two boolean panels.",
140 "Or" => "Logical OR of two boolean panels.",
141 "Add" => "Element-wise `l` + `r`.",
142 "Sub" => "Element-wise `l` - `r`.",
143 "Mul" => "Element-wise `l` * `r`.",
144 "Div" => "Element-wise `l` / `r`.",
145 other => panic!("no description for operator tag `{other}`"),
146 }
147}
148
149fn op_object_schema(
151 tag: &str,
152 description: &str,
153 fields: &[(&str, &str, bool)], ) -> Value {
155 let mut properties = Map::new();
156 properties.insert("op".into(), json!({ "const": tag }));
157 let mut required: Vec<Value> = vec![json!("op")];
158 for (name, kind, req) in fields {
159 properties.insert((*name).into(), field_schema(kind));
160 if *req {
161 required.push(json!(*name));
162 }
163 }
164 json!({
165 "type": "object",
166 "title": tag,
167 "description": description,
168 "properties": Value::Object(properties),
169 "required": required,
170 "additionalProperties": false,
171 })
172}
173
174fn build_spec_schema() -> Value {
176 let mut branches: Vec<Value> = Vec::new();
177
178 branches.push(op_object_schema(
180 "Data",
181 "A raw input series by name.",
182 &[("name", "string", true)],
183 ));
184 branches.push(op_object_schema(
185 "Const",
186 "A constant scalar value, broadcast across the panel.",
187 &[("value", "number", true)],
188 ));
189
190 for op in meta::function_ops() {
192 let fields: Vec<(&str, &str, bool)> = op
193 .fields
194 .iter()
195 .map(|f| (f.name, f.kind, f.required))
196 .collect();
197 branches.push(op_object_schema(op.tag, op.description, &fields));
198 }
199
200 for (_, tag) in meta::binary_operators() {
202 branches.push(op_object_schema(
203 tag,
204 binop_description(tag),
205 &[("l", "expr", true), ("r", "expr", true)],
206 ));
207 }
208
209 branches.push(op_object_schema(
211 "Neg",
212 "Negation (-`of`).",
213 &[("of", "expr", true)],
214 ));
215 branches.push(op_object_schema(
216 "Not",
217 "Logical NOT of a boolean panel (NaN is falsy, so `not` of NaN is 1).",
218 &[("of", "expr", true)],
219 ));
220
221 json!({
222 "$schema": "https://json-schema.org/draft/2020-12/schema",
223 "$id": "https://github.com/citrusquant/citrusquant/schema/lemon-spec.schema.json",
224 "title": "Lemon strategy spec (Expr tree)",
225 "description": "The serializable lemon strategy AST: a JSON tree tagged by \"op\". Generated from crates/lemon metadata; do not edit by hand.",
226 "$ref": "#/$defs/Expr",
227 "$defs": {
228 "Expr": {
229 "oneOf": branches
230 }
231 }
232 })
233}
234
235fn schema_dir() -> PathBuf {
237 let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
239 manifest
240 .parent() .and_then(|p| p.parent()) .expect("crate is two levels below workspace root")
243 .join("schema")
244}
245
246fn write_pretty(path: &std::path::Path, value: &Value) {
247 let mut s = serde_json::to_string_pretty(value).expect("serialize");
248 s.push('\n');
249 std::fs::write(path, s).unwrap_or_else(|e| panic!("write {}: {e}", path.display()));
250 println!("wrote {}", path.display());
251}
252
253fn main() {
254 let dir = schema_dir();
255 std::fs::create_dir_all(&dir).expect("create schema dir");
256
257 write_pretty(&dir.join("op-catalog.json"), &build_catalog());
258 write_pretty(&dir.join("lemon-spec.schema.json"), &build_spec_schema());
259
260 let schema = build_spec_schema();
263 let branches = schema["$defs"]["Expr"]["oneOf"].as_array().unwrap();
264 let tags: std::collections::HashSet<&str> = branches
265 .iter()
266 .map(|b| b["properties"]["op"]["const"].as_str().unwrap())
267 .collect();
268 for tag in meta::ALL_OP_TAGS {
269 assert!(tags.contains(tag), "schema missing op tag `{tag}`");
270 }
271 assert_eq!(
272 tags.len(),
273 meta::ALL_OP_TAGS.len(),
274 "schema branch count != ALL_OP_TAGS count"
275 );
276 println!("ok: {} op tags covered", tags.len());
277}