1use std::fmt;
18
19use serde_json::{Map, Value as JsonValue};
20
21use crate::catalog::{Catalog, CatalogEntry, Kind};
22use crate::program::{Action, Condition, ModifyOp, Value};
23use crate::rules::content::{
24 ASSIGN_MEMBER_CALL, GLOBAL_VARIABLE_CALL, MEMBER_ACCESS_CALL, MODIFY_MEMBER_CALL,
25 PLAYER_VARIABLE_CALL, RAW_WORKSHOP_ACTION_CALL,
26};
27use crate::wir::{self, AMBIGUOUS_ENUM_CALL};
28
29#[derive(Debug, Clone)]
31#[non_exhaustive]
32pub enum Statement {
33 Condition(Condition),
35 Action(Action),
38}
39
40#[derive(Debug, Clone, PartialEq)]
44#[non_exhaustive]
45pub enum StatementContentError {
46 UnknownId {
48 id: String,
49 path: String,
50 node: JsonValue,
51 },
52 ArgumentCount {
55 id: String,
56 min: usize,
57 max: Option<usize>,
58 supplied: usize,
59 path: String,
60 node: JsonValue,
61 },
62 InternalSentinel {
64 id: String,
65 path: String,
66 node: JsonValue,
67 },
68 Unrepresentable {
70 reason: String,
71 path: String,
72 node: JsonValue,
73 },
74 Spans {
76 key: String,
77 path: String,
78 node: JsonValue,
79 },
80}
81
82impl StatementContentError {
83 pub fn path(&self) -> &str {
85 match self {
86 Self::UnknownId { path, .. }
87 | Self::ArgumentCount { path, .. }
88 | Self::InternalSentinel { path, .. }
89 | Self::Unrepresentable { path, .. }
90 | Self::Spans { path, .. } => path,
91 }
92 }
93
94 pub fn node(&self) -> &JsonValue {
95 match self {
96 Self::UnknownId { node, .. }
97 | Self::ArgumentCount { node, .. }
98 | Self::InternalSentinel { node, .. }
99 | Self::Unrepresentable { node, .. }
100 | Self::Spans { node, .. } => node,
101 }
102 }
103}
104
105impl fmt::Display for StatementContentError {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 match self {
108 Self::UnknownId { id, path, .. } => {
109 write!(f, "unknown rule-content-v1 id '{id}' at '{path}'")
110 }
111 Self::ArgumentCount {
112 id,
113 min,
114 max,
115 supplied,
116 path,
117 ..
118 } => {
119 let max = max.map_or_else(|| "+".to_string(), |max| format!("..{max}"));
120 write!(
121 f,
122 "'{id}' at '{path}' expects {min}{max} argument(s), got {supplied}"
123 )
124 }
125 Self::InternalSentinel { id, path, .. } => {
126 write!(f, "internal name '{id}' at '{path}' is not rule-content-v1")
127 }
128 Self::Unrepresentable { reason, path, .. } => {
129 write!(f, "node at '{path}' is not representable: {reason}")
130 }
131 Self::Spans { key, path, .. } => {
132 write!(f, "'{key}' at '{path}': spans are output-only")
133 }
134 }
135 }
136}
137
138impl std::error::Error for StatementContentError {}
139
140type Result<T> = std::result::Result<T, StatementContentError>;
141
142pub fn statement_from_content(catalog: &Catalog, node: &JsonValue) -> Result<Statement> {
163 let converter = Converter { catalog };
164 let object = converter.object(node, "")?;
165 if object.contains_key("value") {
166 converter.condition(object, node)
167 } else if object.contains_key("call") {
168 converter.action(object, node)
169 } else {
170 Err(unrepresentable(
171 "a statement has a 'value' (condition) or a 'call' (action)",
172 "",
173 node,
174 ))
175 }
176}
177
178struct Converter<'a> {
179 catalog: &'a Catalog,
180}
181
182fn unrepresentable(
183 reason: impl Into<String>,
184 path: &str,
185 node: &JsonValue,
186) -> StatementContentError {
187 StatementContentError::Unrepresentable {
188 reason: reason.into(),
189 path: path.to_string(),
190 node: node.clone(),
191 }
192}
193
194fn child(path: &str, segment: impl fmt::Display) -> String {
195 format!("{path}/{segment}")
196}
197
198impl Converter<'_> {
199 fn object<'n>(&self, node: &'n JsonValue, path: &str) -> Result<&'n Map<String, JsonValue>> {
200 let Some(object) = node.as_object() else {
201 return Err(unrepresentable("expected a JSON object", path, node));
202 };
203 for key in ["span", "spans"] {
204 if object.contains_key(key) {
205 return Err(StatementContentError::Spans {
206 key: key.to_string(),
207 path: path.to_string(),
208 node: node.clone(),
209 });
210 }
211 }
212 Ok(object)
213 }
214
215 fn only_keys(
216 &self,
217 object: &Map<String, JsonValue>,
218 allowed: &[&str],
219 path: &str,
220 node: &JsonValue,
221 ) -> Result<()> {
222 match object.keys().find(|key| !allowed.contains(&key.as_str())) {
223 Some(key) => Err(unrepresentable(
224 format!("unexpected key '{key}'"),
225 path,
226 node,
227 )),
228 None => Ok(()),
229 }
230 }
231
232 fn disabled(&self, object: &Map<String, JsonValue>, node: &JsonValue) -> Result<bool> {
233 match object.get("disabled") {
234 None => Ok(false),
235 Some(JsonValue::Bool(true)) => Ok(true),
236 Some(_) => Err(unrepresentable(
237 "'disabled' is present only as true",
238 "",
239 node,
240 )),
241 }
242 }
243
244 fn condition(&self, object: &Map<String, JsonValue>, node: &JsonValue) -> Result<Statement> {
245 self.only_keys(object, &["value", "disabled"], "", node)?;
246 let disabled = self.disabled(object, node)?;
247 let value = self.value(&object["value"], "/value")?;
248 Ok(Statement::Condition(if disabled {
249 Condition::disabled(value)
250 } else {
251 Condition::new(value)
252 }))
253 }
254
255 fn action(&self, object: &Map<String, JsonValue>, node: &JsonValue) -> Result<Statement> {
256 self.only_keys(object, &["call", "args", "disabled"], "", node)?;
257 let disabled = self.disabled(object, node)?;
258 let action = self.action_call(object, node, "")?;
259 Ok(Statement::Action(if disabled {
260 Action::disabled(action)
261 } else {
262 action
263 }))
264 }
265
266 fn call_parts<'n>(
267 &self,
268 object: &'n Map<String, JsonValue>,
269 node: &JsonValue,
270 path: &str,
271 ) -> Result<(&'n str, &'n [JsonValue])> {
272 let id = object
273 .get("call")
274 .and_then(JsonValue::as_str)
275 .ok_or_else(|| unrepresentable("'call' must be a string id", path, node))?;
276 let args = object
277 .get("args")
278 .and_then(JsonValue::as_array)
279 .ok_or_else(|| unrepresentable("a call node needs an 'args' array", path, node))?;
280 if id == AMBIGUOUS_ENUM_CALL {
281 return Err(StatementContentError::InternalSentinel {
282 id: id.to_string(),
283 path: path.to_string(),
284 node: node.clone(),
285 });
286 }
287 Ok((id, args))
288 }
289
290 fn action_call(
291 &self,
292 object: &Map<String, JsonValue>,
293 node: &JsonValue,
294 path: &str,
295 ) -> Result<Action> {
296 let (id, args) = self.call_parts(object, node, path)?;
297 let arg_path = |index: usize| child(&child(path, "args"), index);
298 let unknown = || StatementContentError::UnknownId {
299 id: id.to_string(),
300 path: path.to_string(),
301 node: node.clone(),
302 };
303 let count = |min: usize, max: usize| -> Result<()> {
304 if (min..=max).contains(&args.len()) {
305 Ok(())
306 } else {
307 Err(StatementContentError::ArgumentCount {
308 id: id.to_string(),
309 min,
310 max: Some(max),
311 supplied: args.len(),
312 path: path.to_string(),
313 node: node.clone(),
314 })
315 }
316 };
317 let value = |index: usize| self.value(&args[index], &arg_path(index));
318 let variable = |index: usize| self.variable(&args[index], &arg_path(index));
319 let op = |index: usize| self.modify_op(&args[index], &arg_path(index));
320
321 let action = match id {
322 RAW_WORKSHOP_ACTION_CALL => {
323 return Err(unrepresentable(
324 "an opaque raw Workshop action has no canonical content",
325 path,
326 node,
327 ));
328 }
329 "setGlobalVariable" => {
330 count(2, 2)?;
331 Action::SetGlobalVariable {
332 variable: variable(0)?,
333 value: value(1)?,
334 }
335 }
336 "modifyGlobalVariable" => {
337 count(3, 3)?;
338 Action::ModifyGlobalVariable {
339 variable: variable(0)?,
340 op: op(1)?,
341 value: value(2)?,
342 }
343 }
344 "setPlayerVariable" => {
345 count(3, 3)?;
346 Action::SetPlayerVariable {
347 player: value(0)?,
348 variable: variable(1)?,
349 value: value(2)?,
350 }
351 }
352 "modifyPlayerVariable" => {
353 count(4, 4)?;
354 Action::ModifyPlayerVariable {
355 player: value(0)?,
356 variable: variable(1)?,
357 op: op(2)?,
358 value: value(3)?,
359 }
360 }
361 ASSIGN_MEMBER_CALL => {
362 count(2, 2)?;
363 Action::AssignMember {
364 target: value(0)?,
365 op: None,
366 value: value(1)?,
367 }
368 }
369 MODIFY_MEMBER_CALL => {
370 count(3, 3)?;
371 Action::AssignMember {
372 target: value(0)?,
373 op: Some(op(1)?),
374 value: value(2)?,
375 }
376 }
377 "callSubroutine" => {
378 count(1, 1)?;
379 let JsonValue::Object(subroutine) = &args[0] else {
380 return Err(unrepresentable(
381 "expected a {\"subroutine\"} node",
382 &arg_path(0),
383 &args[0],
384 ));
385 };
386 self.object(&args[0], &arg_path(0))?;
387 let name = subroutine.get("subroutine").and_then(JsonValue::as_str);
388 match (name, subroutine.len()) {
389 (Some(name), 1) => Action::CallSubroutine {
390 subroutine: name.to_string(),
391 },
392 _ => {
393 return Err(unrepresentable(
394 "expected a {\"subroutine\"} node",
395 &arg_path(0),
396 &args[0],
397 ));
398 }
399 }
400 }
401 "if" => {
402 count(1, 1)?;
403 Action::If {
404 condition: value(0)?,
405 }
406 }
407 "elseIf" => {
408 count(1, 1)?;
409 Action::ElseIf {
410 condition: value(0)?,
411 }
412 }
413 "while" => {
414 count(1, 1)?;
415 Action::While {
416 condition: value(0)?,
417 }
418 }
419 "else" => {
420 count(0, 0)?;
421 Action::Else
422 }
423 "end" => {
424 count(0, 0)?;
425 Action::End
426 }
427 "forGlobalVariable" => {
428 count(4, 4)?;
429 Action::ForGlobalVariable {
430 variable: variable(0)?,
431 start: value(1)?,
432 stop: value(2)?,
433 step: value(3)?,
434 }
435 }
436 "forPlayerVariable" => {
437 count(5, 5)?;
438 Action::ForPlayerVariable {
439 player: value(0)?,
440 variable: variable(1)?,
441 start: value(2)?,
442 stop: value(3)?,
443 step: value(4)?,
444 }
445 }
446 _ => {
447 let entry = self.catalog.entry(Kind::Action, id).ok_or_else(unknown)?;
448 let values = self.values(args, path)?;
449 self.check_arity(entry, &values, node, path)?;
450 let name = match (id, values.first()) {
451 ("chasePlayerVariableAtRate", Some(Value::PlayerVariable { .. })) => {
452 "chaseAtRate"
453 }
454 ("chasePlayerVariableOverTime", Some(Value::PlayerVariable { .. })) => {
455 "chaseOverTime"
456 }
457 ("chasePlayerVariableAtRate" | "chasePlayerVariableOverTime", _) => {
458 return Err(unrepresentable(
459 "a player-variable chase takes a playerVariable target",
460 &arg_path(0),
461 &args[0],
462 ));
463 }
464 ("chaseAtRate" | "chaseOverTime", Some(Value::PlayerVariable { .. })) => {
465 return Err(unrepresentable(
466 "a playerVariable chase is named chasePlayerVariable…",
467 path,
468 node,
469 ));
470 }
471 _ => id,
472 };
473 Action::call(name, values)
474 }
475 };
476 Ok(action)
477 }
478
479 fn check_arity(
480 &self,
481 entry: &CatalogEntry,
482 values: &[Value],
483 node: &JsonValue,
484 path: &str,
485 ) -> Result<()> {
486 let folded = entry.folds_player_variable_pair()
487 && matches!(values.first(), Some(Value::PlayerVariable { .. }));
488 let supplied = values.len() + usize::from(folded);
489 if let Some((min, max)) = entry.rejected_arity(supplied) {
490 return Err(StatementContentError::ArgumentCount {
491 id: entry.id.clone(),
492 min,
493 max,
494 supplied,
495 path: path.to_string(),
496 node: node.clone(),
497 });
498 }
499 Ok(())
500 }
501
502 fn variable(&self, node: &JsonValue, path: &str) -> Result<String> {
503 let object = self.object(node, path)?;
504 match (
505 object.get("variable").and_then(JsonValue::as_str),
506 object.len(),
507 ) {
508 (Some(name), 1) => Ok(name.to_string()),
509 _ => Err(unrepresentable(
510 "expected a {\"variable\"} node",
511 path,
512 node,
513 )),
514 }
515 }
516
517 fn modify_op(&self, node: &JsonValue, path: &str) -> Result<ModifyOp> {
518 let object = self.object(node, path)?;
519 let (id, args) = self.call_parts(object, node, path)?;
520 self.only_keys(object, &["call", "args"], path, node)?;
521 if !args.is_empty() {
522 return Err(StatementContentError::ArgumentCount {
523 id: id.to_string(),
524 min: 0,
525 max: Some(0),
526 supplied: args.len(),
527 path: path.to_string(),
528 node: node.clone(),
529 });
530 }
531 ModifyOp::from_catalog_id(id)
532 .ok_or_else(|| unrepresentable(format!("'{id}' is not a modify operation"), path, node))
533 }
534
535 fn value(&self, node: &JsonValue, path: &str) -> Result<Value> {
536 match node {
537 JsonValue::Number(number) => Ok(Value::Number(number.as_f64().ok_or_else(|| {
538 unrepresentable("number is not representable as f64", path, node)
539 })?)),
540 JsonValue::Bool(value) => Ok(Value::Bool(*value)),
541 JsonValue::Null => Ok(Value::Null),
542 JsonValue::Object(object) => self.object_value(object, node, path),
543 _ => Err(unrepresentable("expected a value node", path, node)),
544 }
545 }
546
547 fn object_value(
548 &self,
549 object: &Map<String, JsonValue>,
550 node: &JsonValue,
551 path: &str,
552 ) -> Result<Value> {
553 self.object(node, path)?;
554 let text = |key: &str| object.get(key).and_then(JsonValue::as_str);
555 if object.contains_key("call") {
556 self.only_keys(object, &["call", "args"], path, node)?;
557 return self.value_call(object, node, path);
558 }
559 let tagged = |keys: &[&str]| -> Result<()> {
560 if object.len() == keys.len() && keys.iter().all(|key| object.contains_key(*key)) {
561 Ok(())
562 } else {
563 Err(unrepresentable("malformed tagged node", path, node))
564 }
565 };
566 if object.contains_key("string") {
567 tagged(&["string"])?;
568 let string = text("string")
569 .ok_or_else(|| unrepresentable("'string' must be text", path, node))?;
570 return Ok(Value::string(string));
571 }
572 if object.contains_key("localizedString") {
573 tagged(&["localizedString"])?;
574 let id = text("localizedString")
575 .ok_or_else(|| unrepresentable("'localizedString' must be an id", path, node))?;
576 if !self.catalog.localized_strings().any(|entry| entry.id == id) {
577 return Err(StatementContentError::UnknownId {
578 id: id.to_string(),
579 path: path.to_string(),
580 node: node.clone(),
581 });
582 }
583 return Ok(Value::LocalizedString(id.to_string()));
584 }
585 if object.contains_key("enum") {
586 tagged(&["enum", "member"])?;
587 let (Some(domain), Some(member)) = (text("enum"), text("member")) else {
588 return Err(unrepresentable("enum and member must be text", path, node));
589 };
590 let known = self
591 .catalog
592 .enum_domain(domain)
593 .is_some_and(|domain| domain.members.iter().any(|m| m.member == member));
594 if !known {
595 return Err(StatementContentError::UnknownId {
596 id: format!("{domain}.{member}"),
597 path: path.to_string(),
598 node: node.clone(),
599 });
600 }
601 return Ok(Value::Enum {
602 value_type: domain.to_string(),
603 value: member.to_string(),
604 });
605 }
606 if object.contains_key("ambiguous") {
607 return Err(unrepresentable(
608 "an ambiguous enum member has no canonical identity to write",
609 path,
610 node,
611 ));
612 }
613 if object.contains_key("subroutine") {
614 tagged(&["subroutine"])?;
615 let name = text("subroutine")
616 .ok_or_else(|| unrepresentable("'subroutine' must be a name", path, node))?;
617 return Ok(Value::Subroutine(name.to_string()));
618 }
619 Err(unrepresentable("unknown node form", path, node))
620 }
621
622 fn value_call(
623 &self,
624 object: &Map<String, JsonValue>,
625 node: &JsonValue,
626 path: &str,
627 ) -> Result<Value> {
628 let (id, args) = self.call_parts(object, node, path)?;
629 let arg_path = |index: usize| child(&child(path, "args"), index);
630 let count_error = |min: usize, max: Option<usize>| StatementContentError::ArgumentCount {
631 id: id.to_string(),
632 min,
633 max,
634 supplied: args.len(),
635 path: path.to_string(),
636 node: node.clone(),
637 };
638 match id {
639 GLOBAL_VARIABLE_CALL => {
640 if args.len() != 1 {
641 return Err(count_error(1, Some(1)));
642 }
643 Ok(Value::GlobalVariable(
644 self.variable(&args[0], &arg_path(0))?,
645 ))
646 }
647 PLAYER_VARIABLE_CALL => {
648 if args.len() != 2 {
649 return Err(count_error(2, Some(2)));
650 }
651 Ok(Value::player_variable(
652 self.value(&args[0], &arg_path(0))?,
653 self.variable(&args[1], &arg_path(1))?,
654 ))
655 }
656 MEMBER_ACCESS_CALL => {
657 if !(2..=3).contains(&args.len()) {
658 return Err(count_error(2, Some(3)));
659 }
660 let values = self.values(args, path)?;
661 if !matches!(values[1], Value::String(_)) {
662 return Err(unrepresentable(
663 "memberAccess member must be a string",
664 &arg_path(1),
665 &args[1],
666 ));
667 }
668 Ok(Value::Call {
669 name: id.to_string(),
670 args: values,
671 })
672 }
673 _ => {
674 let values = self.values(args, path)?;
675 let operand = args.is_empty() && wir::is_operation_helper(id);
676 if !operand && let Some(entry) = self.catalog.entry(Kind::Value, id) {
677 self.check_arity(entry, &values, node, path)?;
678 return Ok(self.shaped_call(id, values));
679 }
680 if self.catalog.entry(Kind::Operator, id).is_some()
681 || wir::is_canonical_helper_call(id, args.len())
682 {
683 return Ok(Value::Call {
684 name: id.to_string(),
685 args: values,
686 });
687 }
688 if wir::is_canonical_helper(id) {
689 return Err(count_error(0, Some(0)));
690 }
691 Err(StatementContentError::UnknownId {
692 id: id.to_string(),
693 path: path.to_string(),
694 node: node.clone(),
695 })
696 }
697 }
698 }
699
700 fn values(&self, args: &[JsonValue], path: &str) -> Result<Vec<Value>> {
701 args.iter()
702 .enumerate()
703 .map(|(index, arg)| self.value(arg, &child(&child(path, "args"), index)))
704 .collect()
705 }
706
707 fn shaped_call(&self, id: &str, mut values: Vec<Value>) -> Value {
708 match (id, values.len()) {
709 ("eventPlayer", 0) => Value::EventPlayer,
710 ("array", _) => Value::Array(values),
711 ("vector", 3) => {
712 let z = Box::new(values.pop().expect("three values"));
713 let y = Box::new(values.pop().expect("three values"));
714 let x = Box::new(values.pop().expect("three values"));
715 Value::Vector { x, y, z }
716 }
717 _ => Value::call(id, values),
718 }
719 }
720}