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    /// Extra `add`-style fields that apply only for certain `type` values.
87    #[serde(default)]
88    pub variant_parameter_groups: Vec<VariantParameterGroup>,
89    #[serde(default)]
90    pub return_values: Vec<Parameter>,
91    #[serde(default)]
92    pub format: MethodFormat,
93}
94
95/// Factorio `variant_parameter_groups` entry on a takes-table method.
96#[derive(Debug, Deserialize)]
97pub struct VariantParameterGroup {
98    pub name: String,
99    #[serde(default)]
100    pub description: String,
101    #[serde(default)]
102    pub parameters: Vec<Parameter>,
103}
104
105#[derive(Debug, Default, Deserialize)]
106pub struct MethodFormat {
107    #[serde(default)]
108    pub takes_table: bool,
109}
110
111#[derive(Debug, Deserialize)]
112pub struct Attribute {
113    pub name: String,
114    #[serde(default)]
115    pub description: String,
116    #[serde(default)]
117    pub read_type: Option<ApiType>,
118    #[serde(default)]
119    pub write_type: Option<ApiType>,
120    #[serde(default)]
121    pub optional: bool,
122}
123
124#[derive(Debug, Clone, Deserialize)]
125pub struct Parameter {
126    #[serde(default)]
127    pub name: String,
128    #[serde(default)]
129    pub description: String,
130    #[serde(rename = "type")]
131    pub type_name: ApiType,
132    #[serde(default)]
133    pub optional: bool,
134    /// Factorio call / field order (JSON array order is not always correct).
135    #[serde(default)]
136    pub order: u32,
137}
138
139#[derive(Debug, Clone, Deserialize)]
140#[serde(transparent)]
141pub struct ApiType(pub serde_json::Value);
142
143impl ApiType {
144    pub fn as_simple_name(&self) -> Option<&str> {
145        self.0.as_str()
146    }
147
148    pub fn complex_type(&self) -> Option<&str> {
149        self.0.get("complex_type").and_then(|value| value.as_str())
150    }
151
152    pub fn child_type(&self, key: &str) -> Option<ApiType> {
153        self.0.get(key).cloned().map(ApiType)
154    }
155
156    pub fn options(&self) -> Vec<ApiType> {
157        self.0
158            .get("options")
159            .and_then(|value| value.as_array())
160            .map(|values| values.iter().cloned().map(ApiType).collect())
161            .unwrap_or_default()
162    }
163
164    /// Element types of a `tuple` complex type (from the `"values"` array).
165    pub fn tuple_values(&self) -> Vec<ApiType> {
166        self.0
167            .get("values")
168            .and_then(|v| v.as_array())
169            .map(|values| values.iter().cloned().map(ApiType).collect())
170            .unwrap_or_default()
171    }
172
173    /// For a `literal` complex type, returns the primitive kind: `"string"`,
174    /// `"number"`, or `"boolean"`, based on the JSON type of the `"value"` field.
175    ///
176    /// Returns `None` for non-literal types (e.g. `array`, `union`) even if they
177    /// happen to have a `"value"` key - the `complex_type` must be `"literal"`.
178    pub fn literal_kind(&self) -> Option<&'static str> {
179        // Guard: only `literal` complex types are valid here.
180        if self.complex_type() != Some("literal") {
181            return None;
182        }
183        let value = self.0.get("value")?;
184        if value.is_string() {
185            Some("string")
186        } else if value.is_number() {
187            Some("number")
188        } else if value.is_boolean() {
189            Some("boolean")
190        } else {
191            None
192        }
193    }
194
195    /// Parameters of a `table` complex type, as `(name, type, optional)` triples.
196    pub fn parameters(&self) -> Vec<(String, ApiType, bool)> {
197        self.0
198            .get("parameters")
199            .and_then(|value| value.as_array())
200            .map(|params| {
201                params
202                    .iter()
203                    .filter_map(|p| {
204                        let name = p.get("name")?.as_str()?.to_string();
205                        let ty = ApiType(p.get("type")?.clone());
206                        let optional = p.get("optional").and_then(|v| v.as_bool()).unwrap_or(false);
207                        Some((name, ty, optional))
208                    })
209                    .collect()
210            })
211            .unwrap_or_default()
212    }
213
214    /// Attributes of a `LuaStruct` complex type, as `(name, read/write type, optional)`.
215    ///
216    /// Prefers `read_type` when present, otherwise `write_type`.
217    pub fn attributes(&self) -> Vec<(String, ApiType, bool)> {
218        self.0
219            .get("attributes")
220            .and_then(|value| value.as_array())
221            .map(|attrs| {
222                attrs
223                    .iter()
224                    .filter_map(|a| {
225                        let name = a.get("name")?.as_str()?.to_string();
226                        let ty = a
227                            .get("read_type")
228                            .or_else(|| a.get("write_type"))
229                            .cloned()
230                            .map(ApiType)?;
231                        let optional = a.get("optional").and_then(|v| v.as_bool()).unwrap_or(false);
232                        Some((name, ty, optional))
233                    })
234                    .collect()
235            })
236            .unwrap_or_default()
237    }
238
239    /// Whether this dictionary maps keys to the literal `true` (Factorio flag sets).
240    pub fn is_flag_set_dictionary(&self) -> bool {
241        if self.complex_type() != Some("dictionary") {
242            return false;
243        }
244        let Some(value) = self.child_type("value") else {
245            return false;
246        };
247        value.complex_type() == Some("literal")
248            && value.0.get("value").and_then(serde_json::Value::as_bool) == Some(true)
249    }
250
251    /// Non-`nil` arms of a `union` complex type.
252    pub fn non_nil_options(&self) -> Vec<ApiType> {
253        self.options()
254            .into_iter()
255            .filter(|option| option.as_simple_name() != Some("nil"))
256            .collect()
257    }
258
259    /// Whether this type is a `union` whose non-nil arms are all string literals.
260    pub fn is_homog_string_literal_union(&self) -> bool {
261        if self.complex_type() != Some("union") {
262            return false;
263        }
264        let non_nil = self.non_nil_options();
265        !non_nil.is_empty()
266            && non_nil
267                .iter()
268                .all(|option| option.literal_kind() == Some("string"))
269    }
270
271    /// String values of a homogeneous string-literal union, in Factorio order.
272    ///
273    /// Returns an empty vec when this is not a homog string-literal union.
274    pub fn string_literal_values(&self) -> Vec<String> {
275        if !self.is_homog_string_literal_union() {
276            return Vec::new();
277        }
278        self.non_nil_options()
279            .into_iter()
280            .filter_map(|option| {
281                option
282                    .0
283                    .get("value")
284                    .and_then(|value| value.as_str())
285                    .map(str::to_string)
286            })
287            .collect()
288    }
289
290    /// Whether this union includes a `nil` arm.
291    pub fn union_has_nil(&self) -> bool {
292        self.complex_type() == Some("union")
293            && self
294                .options()
295                .iter()
296                .any(|option| option.as_simple_name() == Some("nil"))
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::ApiType;
303
304    fn parse(json: &str) -> ApiType {
305        ApiType(serde_json::from_str(json).expect("json"))
306    }
307
308    #[test]
309    fn detects_homog_string_literal_union() {
310        let ty = parse(
311            r#"{
312                "complex_type": "union",
313                "options": [
314                    {"complex_type": "literal", "value": "left"},
315                    {"complex_type": "literal", "value": "right"},
316                    "nil"
317                ]
318            }"#,
319        );
320        assert!(ty.is_homog_string_literal_union());
321        assert!(ty.union_has_nil());
322        assert_eq!(
323            ty.string_literal_values(),
324            vec!["left".to_string(), "right".to_string()]
325        );
326    }
327
328    #[test]
329    fn rejects_heterogeneous_union() {
330        let ty = parse(
331            r#"{
332                "complex_type": "union",
333                "options": ["LuaEntity", "LuaEquipment"]
334            }"#,
335        );
336        assert!(!ty.is_homog_string_literal_union());
337        assert!(ty.string_literal_values().is_empty());
338    }
339
340    #[test]
341    fn rejects_non_union() {
342        let ty = parse(r#""string""#);
343        assert!(!ty.is_homog_string_literal_union());
344    }
345}