1use 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#[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#[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#[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
143pub 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#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
154pub struct NativeBlock {
155 pub text: String,
156 pub span: SourceSpan,
157}
158
159#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
163pub struct Clause {
164 pub text: String,
165 pub span: SourceSpan,
166}
167
168#[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 Text,
177 Integer {
178 min: u32,
179 max: u32,
180 },
181 Bool,
182 Choice {
183 options: &'static [&'static str],
184 },
185 Identifiers,
187 Effects,
189 Conditions,
191 List {
195 element: &'static ValueType,
196 },
197}
198
199impl ValueType {
200 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
234pub 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
269fn 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
390pub(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}