Skip to main content

foch_annotation/
value.rs

1//! Typed annotation values.
2//!
3//! Newtypes such as [`GameDate`] and [`Tag`] can only be built through their
4//! parsers, so consumers never re-validate them.
5
6use crate::source::{RelPath, SourceSpan};
7use foch::game::eu4::script::parser::{AstStatement, AstValue, ScalarValue};
8use foch::model::GamePath;
9use jomini::common::{Date, PdsDate};
10use serde::{Deserialize, Serialize};
11use std::fmt;
12
13macro_rules! string_newtype {
14	($name:ident) => {
15		impl $name {
16			pub fn as_str(&self) -> &str {
17				&self.0
18			}
19		}
20
21		impl TryFrom<String> for $name {
22			type Error = String;
23
24			fn try_from(value: String) -> Result<Self, String> {
25				Self::parse(&value)
26			}
27		}
28
29		impl From<$name> for String {
30			fn from(value: $name) -> Self {
31				value.0
32			}
33		}
34
35		impl fmt::Display for $name {
36			fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37				formatter.write_str(&self.0)
38			}
39		}
40	};
41}
42
43/// A full EU4 calendar date (no leap years), formatted `YYYY.M.D`.
44#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
45#[serde(try_from = "String", into = "String")]
46pub struct GameDate(Date);
47
48impl GameDate {
49	pub fn parse(value: &str) -> Result<Self, String> {
50		let parts: Vec<_> = value.split('.').collect();
51		if parts.len() != 3
52			|| parts
53				.iter()
54				.any(|part| part.is_empty() || !part.bytes().all(|byte| byte.is_ascii_digit()))
55		{
56			return Err("date must be a full date in YYYY.M.D format".into());
57		}
58		let date = Date::parse(value).map_err(|_| "invalid EU4 calendar date")?;
59		if !(1..=9999).contains(&date.year()) {
60			return Err("date year must be in 1..=9999".into());
61		}
62		Ok(Self(date))
63	}
64
65	pub fn add_days(self, days: u32) -> Result<Self, String> {
66		let last = Date::from_ymd(9999, 12, 31);
67		if i64::from(days) > i64::from(self.0.days_until(&last)) {
68			return Err("date would exceed year 9999".into());
69		}
70		Ok(Self(self.0.add_days(days as i32)))
71	}
72}
73
74impl TryFrom<String> for GameDate {
75	type Error = String;
76
77	fn try_from(value: String) -> Result<Self, String> {
78		Self::parse(&value)
79	}
80}
81
82impl From<GameDate> for String {
83	fn from(value: GameDate) -> Self {
84		value.to_string()
85	}
86}
87
88impl fmt::Display for GameDate {
89	fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90		write!(formatter, "{}", self.0.game_fmt())
91	}
92}
93
94/// Three uppercase ASCII letters/digits starting with a letter, such as `SWE`.
95#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
96#[serde(try_from = "String", into = "String")]
97pub struct Tag(String);
98
99impl Tag {
100	pub fn parse(value: &str) -> Result<Self, String> {
101		if value.len() == 3
102			&& value.as_bytes()[0].is_ascii_uppercase()
103			&& value
104				.bytes()
105				.all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
106		{
107			Ok(Self(value.into()))
108		} else {
109			Err("tag must be three uppercase ASCII letters/digits, starting with a letter".into())
110		}
111	}
112}
113string_newtype!(Tag);
114
115/// `123` or `namespace.123`.
116#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
117#[serde(try_from = "String", into = "String")]
118pub struct EventId(String);
119
120impl EventId {
121	pub fn parse(value: &str) -> Result<Self, String> {
122		let numeric = |part: &str| {
123			!part.is_empty()
124				&& part.bytes().all(|byte| byte.is_ascii_digit())
125				&& part.parse::<u32>().is_ok()
126		};
127		let valid = value.len() <= 128
128			&& (numeric(value)
129				|| value
130					.split_once('.')
131					.is_some_and(|(namespace, number)| identifier(namespace) && numeric(number)));
132		if valid {
133			Ok(Self(value.into()))
134		} else {
135			Err(format!(
136				"invalid event ID {value:?}; expected numeric or namespace.numeric"
137			))
138		}
139	}
140}
141string_newtype!(EventId);
142
143/// An ASCII identifier: a letter or `_`, then letters, digits or `_`.
144pub fn identifier(value: &str) -> bool {
145	let mut bytes = value.bytes();
146	bytes
147		.next()
148		.is_some_and(|first| first.is_ascii_alphabetic() || first == b'_')
149		&& bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
150}
151
152/// Native script, without its outer braces.
153#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
154pub struct NativeBlock {
155	pub text: String,
156	pub span: SourceSpan,
157}
158
159/// One top-level condition of a condition block. Top-level conditions are
160/// conjunctive and side-effect free, so evaluating them separately at the
161/// same moment gives the same verdict as the whole block.
162#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
163pub struct Clause {
164	pub text: String,
165	pub span: SourceSpan,
166}
167
168/// The type of an annotation parameter.
169#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
170#[serde(tag = "type", rename_all = "snake_case")]
171pub enum ValueType {
172	Date,
173	Tag,
174	EventId,
175	/// Any scalar; quote text containing spaces.
176	Text,
177	Integer {
178		min: u32,
179		max: u32,
180	},
181	Bool,
182	Choice {
183		options: &'static [&'static str],
184	},
185	/// A block of identifiers, such as `{ a b }`.
186	Identifiers,
187	/// A block of native effects.
188	Effects,
189	/// A block of native conditions, kept clause by clause.
190	Conditions,
191	/// A block of scalar values each of one element type, such as the
192	/// `{ SWE DAN }` of a parametrized `tag`. Only scalar element types are
193	/// supported.
194	List {
195		element: &'static ValueType,
196	},
197}
198
199impl ValueType {
200	/// Short human description, as shown in completions and hovers.
201	pub fn describe(&self) -> String {
202		match self {
203			Self::Date => "date".into(),
204			Self::Tag => "country tag".into(),
205			Self::EventId => "event ID".into(),
206			Self::Text => "string".into(),
207			Self::Integer { min, max } => format!("integer ({min}..={max})"),
208			Self::Bool => "yes | no".into(),
209			Self::Choice { options } => options.join(" | "),
210			Self::Identifiers => "names".into(),
211			Self::Effects => "effect block".into(),
212			Self::Conditions => "trigger block".into(),
213			Self::List { element } => format!("{{ {} ... }}", element.describe()),
214		}
215	}
216}
217
218#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
219#[serde(tag = "type", content = "value", rename_all = "snake_case")]
220pub enum Value {
221	Date(GameDate),
222	Tag(Tag),
223	EventId(EventId),
224	Text(String),
225	Integer(u32),
226	Bool(bool),
227	Choice(String),
228	Identifiers(Vec<String>),
229	Effects(NativeBlock),
230	Conditions(Vec<Clause>),
231	List(Vec<Value>),
232}
233
234/// Parses a script value as `value_type`. `source` must have the same byte
235/// offsets as the file named by `path`, so spans point at the original text.
236pub fn parse_value(
237	path: &RelPath,
238	source: &str,
239	value: &AstValue,
240	value_type: ValueType,
241) -> Result<Value, String> {
242	Ok(match value_type {
243		ValueType::Date => Value::Date(GameDate::parse(&scalar(value)?)?),
244		ValueType::Tag => Value::Tag(Tag::parse(&scalar(value)?)?),
245		ValueType::EventId => Value::EventId(EventId::parse(&scalar(value)?)?),
246		ValueType::Text => Value::Text(scalar(value)?),
247		ValueType::Integer { min, max } => Value::Integer(
248			scalar(value)?
249				.parse::<u32>()
250				.ok()
251				.filter(|number| (min..=max).contains(number))
252				.ok_or_else(|| format!("expected an integer in {min}..={max}"))?,
253		),
254		ValueType::Bool => Value::Bool(boolean(value)?),
255		ValueType::Choice { options } => {
256			let text = scalar(value)?;
257			if !options.contains(&text.as_str()) {
258				return Err(format!("expected {}, not {text:?}", options.join(" or ")));
259			}
260			Value::Choice(text)
261		}
262		ValueType::Identifiers => Value::Identifiers(identifiers(value)?),
263		ValueType::Effects => Value::Effects(native_block(path, source, value)?),
264		ValueType::Conditions => Value::Conditions(clauses(path, source, value)?),
265		ValueType::List { element } => Value::List(list(path, source, value, element)?),
266	})
267}
268
269/// A block of scalar items, each parsed as `element`. Items must be bare
270/// scalars, such as the `{ SWE DAN }` of a parametrized `tag`.
271fn list(
272	path: &RelPath,
273	source: &str,
274	value: &AstValue,
275	element: &ValueType,
276) -> Result<Vec<Value>, String> {
277	let AstValue::Block { items, .. } = value else {
278		return Err("expected a block of values, such as { a b }".into());
279	};
280	let mut values = Vec::new();
281	for item in items {
282		match item {
283			AstStatement::Comment { .. } => {}
284			AstStatement::Item { value, .. } => {
285				values.push(parse_value(path, source, value, *element)?)
286			}
287			AstStatement::Assignment { .. } => {
288				return Err("expected bare values, not assignments".into());
289			}
290		}
291	}
292	if values.is_empty() {
293		return Err("list must contain at least one value".into());
294	}
295	Ok(values)
296}
297
298pub fn scalar(value: &AstValue) -> Result<String, String> {
299	match value {
300		AstValue::Scalar {
301			value:
302				ScalarValue::Identifier(value) | ScalarValue::String(value) | ScalarValue::Number(value),
303			..
304		} => Ok(value.clone()),
305		AstValue::Scalar {
306			value: ScalarValue::Bool(value),
307			..
308		} => Ok(if *value { "yes" } else { "no" }.into()),
309		AstValue::Block { .. } => Err("expected a scalar value".into()),
310	}
311}
312
313pub fn boolean(value: &AstValue) -> Result<bool, String> {
314	match value {
315		AstValue::Scalar {
316			value: ScalarValue::Bool(value),
317			..
318		} => Ok(*value),
319		_ => Err("expected yes or no".into()),
320	}
321}
322
323fn identifiers(value: &AstValue) -> Result<Vec<String>, String> {
324	let AstValue::Block { items, .. } = value else {
325		return Err("expected a block of names, such as { a b }".into());
326	};
327	let mut names = Vec::new();
328	for item in items {
329		match item {
330			AstStatement::Comment { .. } => {}
331			AstStatement::Item { value, .. } => {
332				let name = scalar(value)?;
333				if !identifier(&name) {
334					return Err(format!("invalid name {name:?}; expected an identifier"));
335				}
336				if names.contains(&name) {
337					return Err(format!("{name} is listed twice"));
338				}
339				names.push(name);
340			}
341			AstStatement::Assignment { .. } => {
342				return Err("expected names, not assignments".into());
343			}
344		}
345	}
346	Ok(names)
347}
348
349fn native_block(path: &RelPath, source: &str, value: &AstValue) -> Result<NativeBlock, String> {
350	let AstValue::Block { items, span } = value else {
351		return Err("expected a native EU4 block".into());
352	};
353	if !items
354		.iter()
355		.any(|item| matches!(item, AstStatement::Assignment { .. }))
356	{
357		return Err("block must contain native EU4 statements".into());
358	}
359	Ok(NativeBlock {
360		text: source[span.start.offset + 1..span.end.offset - 1]
361			.trim()
362			.into(),
363		span: SourceSpan::from_parser(path, span),
364	})
365}
366
367fn clauses(path: &RelPath, source: &str, value: &AstValue) -> Result<Vec<Clause>, String> {
368	let AstValue::Block { items, .. } = value else {
369		return Err("expected a native EU4 block".into());
370	};
371	let mut clauses = Vec::new();
372	for item in items {
373		match item {
374			AstStatement::Comment { .. } => {}
375			AstStatement::Assignment { span, .. } => clauses.push(Clause {
376				text: source[span.start.offset..span.end.offset].trim().into(),
377				span: SourceSpan::from_parser(path, span),
378			}),
379			AstStatement::Item { .. } => {
380				return Err("block must contain native key = value conditions".into());
381			}
382		}
383	}
384	if clauses.is_empty() {
385		return Err("block must contain native EU4 conditions".into());
386	}
387	Ok(clauses)
388}
389
390/// A parser path label for internal parsing; inputs are literals or already
391/// validated relative paths, so construction does not fail in practice.
392pub(crate) fn game_path(text: &str) -> &GamePath {
393	GamePath::new(text).expect("valid game path label")
394}
395
396#[cfg(test)]
397mod tests {
398	use super::*;
399
400	#[test]
401	fn dates_use_the_nonleap_eu4_calendar() {
402		let date = GameDate::parse("1444.2.28").unwrap();
403		assert_eq!(date.add_days(1).unwrap().to_string(), "1444.3.1");
404		assert!(GameDate::parse("1444.2.29").is_err());
405		assert!(GameDate::parse("1444.11").is_err());
406		assert!(GameDate::parse("0.1.1").is_err());
407		assert!(GameDate::parse("9999.12.31").unwrap().add_days(1).is_err());
408	}
409
410	#[test]
411	fn identifiers_reject_script_injection() {
412		assert!(Tag::parse("SWE").is_ok());
413		for bad in ["swe", "SW", "1SW", "SWE }"] {
414			assert!(Tag::parse(bad).is_err(), "{bad:?}");
415		}
416		assert!(EventId::parse("reforms.1").is_ok());
417		assert!(EventId::parse("7").is_ok());
418		for bad in ["", "a.b", "a.1\nlog=x", "a/1", "1a.1"] {
419			assert!(EventId::parse(bad).is_err(), "{bad:?}");
420		}
421	}
422}