Skip to main content

factorio_api_gen/
schema.rs

1use serde::Deserialize;
2
3#[derive(Debug, Deserialize)]
4pub struct RuntimeApi {
5    pub application_version: String,
6    pub api_version: u32,
7    pub classes: Vec<Class>,
8    pub events: Vec<Event>,
9    pub defines: Vec<Define>,
10    pub global_objects: Vec<GlobalObject>,
11    #[serde(default)]
12    pub global_functions: Vec<Method>,
13    #[serde(default)]
14    pub concepts: Vec<Concept>,
15}
16
17#[derive(Debug, Deserialize)]
18pub struct Class {
19    pub name: String,
20    #[serde(default)]
21    pub description: String,
22    #[serde(default)]
23    pub r#abstract: bool,
24    #[serde(default)]
25    pub parent: Option<String>,
26    #[serde(default)]
27    pub methods: Vec<Method>,
28    #[serde(default)]
29    pub attributes: Vec<Attribute>,
30}
31
32#[derive(Debug, Deserialize)]
33pub struct Event {
34    pub name: String,
35    #[serde(default)]
36    pub description: String,
37    #[serde(default)]
38    pub filter: Option<String>,
39    #[serde(default)]
40    pub data: Vec<Parameter>,
41}
42
43#[derive(Debug, Deserialize)]
44pub struct Concept {
45    pub name: String,
46    #[serde(default)]
47    pub description: String,
48    #[serde(rename = "type")]
49    pub type_name: ApiType,
50}
51
52#[derive(Debug, Deserialize)]
53pub struct Define {
54    pub name: String,
55    #[serde(default)]
56    pub description: String,
57    #[serde(default)]
58    pub values: Vec<DefineValue>,
59    #[serde(default)]
60    pub subkeys: Vec<Define>,
61}
62
63#[derive(Debug, Deserialize)]
64pub struct DefineValue {
65    pub name: String,
66    #[serde(default)]
67    pub description: String,
68}
69
70#[derive(Debug, Deserialize)]
71pub struct GlobalObject {
72    pub name: String,
73    #[serde(default)]
74    pub description: String,
75    #[serde(rename = "type")]
76    pub type_name: ApiType,
77}
78
79#[derive(Debug, Deserialize)]
80pub struct Method {
81    pub name: String,
82    #[serde(default)]
83    pub description: String,
84    #[serde(default)]
85    pub parameters: Vec<Parameter>,
86    #[serde(default)]
87    pub return_values: Vec<Parameter>,
88    #[serde(default)]
89    pub format: MethodFormat,
90}
91
92#[derive(Debug, Default, Deserialize)]
93pub struct MethodFormat {
94    #[serde(default)]
95    pub takes_table: bool,
96}
97
98#[derive(Debug, Deserialize)]
99pub struct Attribute {
100    pub name: String,
101    #[serde(default)]
102    pub description: String,
103    #[serde(default)]
104    pub read_type: Option<ApiType>,
105    #[serde(default)]
106    pub write_type: Option<ApiType>,
107    #[serde(default)]
108    pub optional: bool,
109}
110
111#[derive(Debug, Clone, Deserialize)]
112pub struct Parameter {
113    #[serde(default)]
114    pub name: String,
115    #[serde(default)]
116    pub description: String,
117    #[serde(rename = "type")]
118    pub type_name: ApiType,
119    #[serde(default)]
120    pub optional: bool,
121    /// Factorio call / field order (JSON array order is not always correct).
122    #[serde(default)]
123    pub order: u32,
124}
125
126#[derive(Debug, Clone, Deserialize)]
127#[serde(transparent)]
128pub struct ApiType(pub serde_json::Value);
129
130impl ApiType {
131    pub fn as_simple_name(&self) -> Option<&str> {
132        self.0.as_str()
133    }
134
135    pub fn complex_type(&self) -> Option<&str> {
136        self.0.get("complex_type").and_then(|value| value.as_str())
137    }
138
139    pub fn child_type(&self, key: &str) -> Option<ApiType> {
140        self.0.get(key).cloned().map(ApiType)
141    }
142
143    pub fn options(&self) -> Vec<ApiType> {
144        self.0
145            .get("options")
146            .and_then(|value| value.as_array())
147            .map(|values| values.iter().cloned().map(ApiType).collect())
148            .unwrap_or_default()
149    }
150
151    /// Element types of a `tuple` complex type (from the `"values"` array).
152    pub fn tuple_values(&self) -> Vec<ApiType> {
153        self.0
154            .get("values")
155            .and_then(|v| v.as_array())
156            .map(|values| values.iter().cloned().map(ApiType).collect())
157            .unwrap_or_default()
158    }
159
160    /// For a `literal` complex type, returns the primitive kind: `"string"`,
161    /// `"number"`, or `"boolean"`, based on the JSON type of the `"value"` field.
162    ///
163    /// Returns `None` for non-literal types (e.g. `array`, `union`) even if they
164    /// happen to have a `"value"` key - the `complex_type` must be `"literal"`.
165    pub fn literal_kind(&self) -> Option<&'static str> {
166        // Guard: only `literal` complex types are valid here.
167        if self.complex_type() != Some("literal") {
168            return None;
169        }
170        let value = self.0.get("value")?;
171        if value.is_string() {
172            Some("string")
173        } else if value.is_number() {
174            Some("number")
175        } else if value.is_boolean() {
176            Some("boolean")
177        } else {
178            None
179        }
180    }
181
182    /// Parameters of a `table` complex type, as `(name, type, optional)` triples.
183    pub fn parameters(&self) -> Vec<(String, ApiType, bool)> {
184        self.0
185            .get("parameters")
186            .and_then(|value| value.as_array())
187            .map(|params| {
188                params
189                    .iter()
190                    .filter_map(|p| {
191                        let name = p.get("name")?.as_str()?.to_string();
192                        let ty = ApiType(p.get("type")?.clone());
193                        let optional = p.get("optional").and_then(|v| v.as_bool()).unwrap_or(false);
194                        Some((name, ty, optional))
195                    })
196                    .collect()
197            })
198            .unwrap_or_default()
199    }
200
201    /// Attributes of a `LuaStruct` complex type, as `(name, read/write type, optional)`.
202    ///
203    /// Prefers `read_type` when present, otherwise `write_type`.
204    pub fn attributes(&self) -> Vec<(String, ApiType, bool)> {
205        self.0
206            .get("attributes")
207            .and_then(|value| value.as_array())
208            .map(|attrs| {
209                attrs
210                    .iter()
211                    .filter_map(|a| {
212                        let name = a.get("name")?.as_str()?.to_string();
213                        let ty = a
214                            .get("read_type")
215                            .or_else(|| a.get("write_type"))
216                            .cloned()
217                            .map(ApiType)?;
218                        let optional = a.get("optional").and_then(|v| v.as_bool()).unwrap_or(false);
219                        Some((name, ty, optional))
220                    })
221                    .collect()
222            })
223            .unwrap_or_default()
224    }
225
226    /// Whether this dictionary maps keys to the literal `true` (Factorio flag sets).
227    pub fn is_flag_set_dictionary(&self) -> bool {
228        if self.complex_type() != Some("dictionary") {
229            return false;
230        }
231        let Some(value) = self.child_type("value") else {
232            return false;
233        };
234        value.complex_type() == Some("literal")
235            && value.0.get("value").and_then(serde_json::Value::as_bool) == Some(true)
236    }
237
238    /// Non-`nil` arms of a `union` complex type.
239    pub fn non_nil_options(&self) -> Vec<ApiType> {
240        self.options()
241            .into_iter()
242            .filter(|option| option.as_simple_name() != Some("nil"))
243            .collect()
244    }
245
246    /// Whether this type is a `union` whose non-nil arms are all string literals.
247    pub fn is_homog_string_literal_union(&self) -> bool {
248        if self.complex_type() != Some("union") {
249            return false;
250        }
251        let non_nil = self.non_nil_options();
252        !non_nil.is_empty()
253            && non_nil
254                .iter()
255                .all(|option| option.literal_kind() == Some("string"))
256    }
257
258    /// String values of a homogeneous string-literal union, in Factorio order.
259    ///
260    /// Returns an empty vec when this is not a homog string-literal union.
261    pub fn string_literal_values(&self) -> Vec<String> {
262        if !self.is_homog_string_literal_union() {
263            return Vec::new();
264        }
265        self.non_nil_options()
266            .into_iter()
267            .filter_map(|option| {
268                option
269                    .0
270                    .get("value")
271                    .and_then(|value| value.as_str())
272                    .map(str::to_string)
273            })
274            .collect()
275    }
276
277    /// Whether this union includes a `nil` arm.
278    pub fn union_has_nil(&self) -> bool {
279        self.complex_type() == Some("union")
280            && self
281                .options()
282                .iter()
283                .any(|option| option.as_simple_name() == Some("nil"))
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::ApiType;
290
291    fn parse(json: &str) -> ApiType {
292        ApiType(serde_json::from_str(json).expect("json"))
293    }
294
295    #[test]
296    fn detects_homog_string_literal_union() {
297        let ty = parse(
298            r#"{
299                "complex_type": "union",
300                "options": [
301                    {"complex_type": "literal", "value": "left"},
302                    {"complex_type": "literal", "value": "right"},
303                    "nil"
304                ]
305            }"#,
306        );
307        assert!(ty.is_homog_string_literal_union());
308        assert!(ty.union_has_nil());
309        assert_eq!(
310            ty.string_literal_values(),
311            vec!["left".to_string(), "right".to_string()]
312        );
313    }
314
315    #[test]
316    fn rejects_heterogeneous_union() {
317        let ty = parse(
318            r#"{
319                "complex_type": "union",
320                "options": ["LuaEntity", "LuaEquipment"]
321            }"#,
322        );
323        assert!(!ty.is_homog_string_literal_union());
324        assert!(ty.string_literal_values().is_empty());
325    }
326
327    #[test]
328    fn rejects_non_union() {
329        let ty = parse(r#""string""#);
330        assert!(!ty.is_homog_string_literal_union());
331    }
332}