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