sql_schema/
path_template.rs

1/*!
2Parse a migration path into a [PathTemplate] to later resolve the name of a new migration being written.
3*/
4
5pub use ast::{PathTemplate, Semver, TemplateData, UpDown};
6pub use chrono::{DateTime, Utc};
7pub use parser::ParseError;
8
9mod parser {
10    use std::{cmp::Ordering, ops::Range};
11
12    use chrono::NaiveDate;
13    use thiserror::Error;
14    use winnow::{
15        ascii::digit1,
16        combinator::{alt, fail, opt, repeat, separated},
17        error::{StrContext, StrContextValue},
18        stream::AsChar,
19        token::{take_until, take_while},
20        Parser, Result,
21    };
22
23    use super::{
24        ast::{
25            Date, DateTime, DoUndo, EpochTimestamp, PaddedNumber, Segment, SegmentKind, Semver,
26            SubSecond, Time, Timestamp, Token,
27        },
28        PathTemplate, UpDown,
29    };
30
31    #[derive(Error, Debug)]
32    pub struct ParseError {
33        message: String,
34        span: Range<usize>,
35        input: String,
36    }
37
38    impl std::fmt::Display for ParseError {
39        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40            let lines = self.message.split('\n').collect::<Vec<_>>();
41            let m1 = lines[0];
42            let m2 = lines.get(1).copied().unwrap_or(m1);
43            let title =
44                format!("Oops, we couldn't sort out you're migration naming convention: {m1}");
45            let message = annotate_snippets::Level::Error.title(&title).snippet(
46                annotate_snippets::Snippet::source(&self.input)
47                    .fold(true)
48                    .annotation(
49                        annotate_snippets::Level::Error
50                            .span(self.span.clone())
51                            .label(m2),
52                    ),
53            );
54            let renderer = annotate_snippets::Renderer::plain();
55            let rendered = renderer.render(message);
56            rendered.fmt(f)
57        }
58    }
59
60    fn digit_n<'i>(n: usize) -> impl FnMut(&mut &'i str) -> Result<&'i str> {
61        move |input: &mut &'i str| take_while(n, AsChar::is_dec_digit).parse_next(input)
62    }
63
64    fn dot(input: &mut &str) -> Result<Token> {
65        ".".take().value(Token::Dot).parse_next(input)
66    }
67
68    fn underscore(input: &mut &str) -> Result<Token> {
69        "_".take().value(Token::Underscore).parse_next(input)
70    }
71
72    fn dash(input: &mut &str) -> Result<Token> {
73        "-".take().value(Token::Dash).parse_next(input)
74    }
75
76    fn sep(input: &mut &str) -> Result<Token> {
77        alt((dot, underscore, dash)).parse_next(input)
78    }
79
80    fn padded_number(input: &mut &str) -> Result<Token> {
81        digit1
82            .take()
83            .parse_to::<PaddedNumber>()
84            .map(Token::PaddedNumber)
85            .parse_next(input)
86    }
87
88    fn random_number(input: &mut &str) -> Result<Token> {
89        digit1
90            .take()
91            .parse_to::<usize>()
92            .map(Token::RandomNumber)
93            .parse_next(input)
94    }
95
96    fn semver(input: &mut &str) -> Result<Token> {
97        separated(3, digit1, '.')
98            .map(|_: Vec<&str>| ()) // TODO: why is this map needed?
99            .take()
100            .parse_to::<Semver>()
101            .map(Token::Semver)
102            .parse_next(input)
103    }
104
105    /// timestamps should all be after the year 2000
106    const MIN_DATE: NaiveDate = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
107    /// timestamps should all be before the year 2100
108    const MAX_DATE: NaiveDate = NaiveDate::from_ymd_opt(2100, 1, 1).unwrap();
109
110    fn datetime(input: &mut &str) -> Result<Token> {
111        fn year(input: &mut &str) -> Result<i32> {
112            ("20", digit_n(2))
113                .take()
114                .parse_to::<i32>()
115                .parse_next(input)
116        }
117
118        fn month(input: &mut &str) -> Result<u32> {
119            digit_n(2)
120                .parse_to::<u32>()
121                .verify(|mm| *mm <= 12 && *mm > 0)
122                .parse_next(input)
123        }
124
125        fn day(input: &mut &str) -> Result<u32> {
126            digit_n(2)
127                .parse_to::<u32>()
128                .verify(|dd| *dd <= 31 && *dd > 0)
129                .parse_next(input)
130        }
131
132        fn hour(input: &mut &str) -> Result<u32> {
133            digit_n(2)
134                .parse_to::<u32>()
135                .verify(|hh| *hh <= 12 && *hh > 0)
136                .parse_next(input)
137        }
138
139        fn minute(input: &mut &str) -> Result<u32> {
140            digit_n(2)
141                .parse_to::<u32>()
142                .verify(|mm| *mm < 60)
143                .parse_next(input)
144        }
145
146        fn second(input: &mut &str) -> Result<u32> {
147            digit_n(2)
148                .parse_to::<u32>()
149                .verify(|ss| *ss < 60)
150                .parse_next(input)
151        }
152
153        fn submilli(input: &mut &str) -> Result<SubSecond> {
154            take_while(1..=3, AsChar::is_dec_digit)
155                .parse_to::<u32>()
156                .map(SubSecond::Milli)
157                .parse_next(input)
158        }
159
160        fn submicro(input: &mut &str) -> Result<SubSecond> {
161            take_while(6, AsChar::is_dec_digit)
162                .parse_to::<u32>()
163                .map(SubSecond::Micro)
164                .parse_next(input)
165        }
166
167        fn subnano(input: &mut &str) -> Result<SubSecond> {
168            take_while(9, AsChar::is_dec_digit)
169                .parse_to::<u32>()
170                .map(SubSecond::Nano)
171                .parse_next(input)
172        }
173
174        fn sep_literal<'i>(input: &mut &'i str) -> Result<&'i str> {
175            sep.take().parse_next(input)
176        }
177
178        fn time(input: &mut &str) -> Result<Time> {
179            (
180                hour,
181                opt(sep_literal),
182                minute,
183                opt((
184                    opt(sep_literal),
185                    second,
186                    opt((opt(sep_literal), alt((subnano, submicro, submilli)))),
187                )),
188            )
189                .map(|(hour, s1, minute, second)| {
190                    let hour_sep = s1.map(|s| s.to_string());
191                    let (minute_sep, second, second_sep, subsecond) =
192                        if let Some((s2, second, subsec)) = second {
193                            let minute_sep = s2.map(|s| s.to_string());
194
195                            let (second_sep, subsecond) = if let Some((s3, subsecond)) = subsec {
196                                let second_sep = s3.map(|s| s.to_string());
197                                (second_sep, Some(subsecond))
198                            } else {
199                                (None, None)
200                            };
201
202                            (minute_sep, Some(second), second_sep, subsecond)
203                        } else {
204                            (None, None, None, None)
205                        };
206
207                    Time {
208                        hour,
209                        hour_sep,
210                        minute,
211                        minute_sep,
212                        second,
213                        second_sep,
214                        subsecond,
215                    }
216                })
217                .parse_next(input)
218        }
219
220        (
221            year,
222            opt(sep_literal),
223            month,
224            opt(sep_literal),
225            day,
226            opt((opt(sep_literal), time)),
227        )
228            .map(|(year, s1, month, s2, day, time_or_rand)| {
229                let year_sep = s1.map(|s| s.to_string());
230                let month_sep = s2.map(|s| s.to_string());
231                let date = Date {
232                    year,
233                    year_sep,
234                    month,
235                    month_sep,
236                    day,
237                };
238
239                let (date_sep, time) = if let Some((s3, time)) = time_or_rand {
240                    let date_sep = s3.map(|s| s.to_string());
241                    (date_sep, Some(time))
242                } else {
243                    (None, None)
244                };
245
246                Token::Timestamp(Timestamp::DateTime(DateTime {
247                    date,
248                    date_sep,
249                    time,
250                }))
251            })
252            .parse_next(input)
253    }
254
255    fn validate_datetime<Z: chrono::TimeZone>(
256        ts: chrono::DateTime<Z>,
257    ) -> Option<chrono::DateTime<Z>> {
258        if matches!(
259            ts.date_naive().cmp(&MIN_DATE),
260            Ordering::Greater | Ordering::Equal
261        ) && matches!(
262            ts.date_naive().cmp(&MAX_DATE),
263            Ordering::Less | Ordering::Equal
264        ) {
265            Some(ts)
266        } else {
267            None
268        }
269    }
270
271    fn epoch_seconds(input: &mut &str) -> Result<EpochTimestamp> {
272        digit1
273            .take()
274            .parse_to::<i64>()
275            .verify_map(|secs| chrono::DateTime::from_timestamp(secs, 0))
276            .verify_map(validate_datetime)
277            .map(|ts| ts.timestamp())
278            .map(EpochTimestamp::Second)
279            .parse_next(input)
280    }
281
282    fn epoch_millis(input: &mut &str) -> Result<EpochTimestamp> {
283        digit1
284            .take()
285            .parse_to::<i64>()
286            .verify_map(chrono::DateTime::from_timestamp_millis)
287            .verify_map(validate_datetime)
288            .map(|ts| ts.timestamp_millis())
289            .map(EpochTimestamp::Milli)
290            .parse_next(input)
291    }
292
293    fn epoch_micros(input: &mut &str) -> Result<EpochTimestamp> {
294        digit1
295            .take()
296            .parse_to::<i64>()
297            .verify_map(chrono::DateTime::from_timestamp_micros)
298            .verify_map(validate_datetime)
299            .map(|ts| ts.timestamp_micros())
300            .map(EpochTimestamp::Micro)
301            .parse_next(input)
302    }
303
304    fn epoch_nanos(input: &mut &str) -> Result<EpochTimestamp> {
305        digit1
306            .take()
307            .parse_to::<i64>()
308            .map(chrono::DateTime::from_timestamp_nanos)
309            .verify_map(validate_datetime)
310            .verify_map(|ts| ts.timestamp_nanos_opt())
311            .map(EpochTimestamp::Nano)
312            .parse_next(input)
313    }
314
315    fn epoch_timestamp(input: &mut &str) -> Result<Token> {
316        alt((epoch_nanos, epoch_micros, epoch_millis, epoch_seconds))
317            .map(Timestamp::Epoch)
318            .map(Token::Timestamp)
319            .parse_next(input)
320    }
321
322    fn name(input: &mut &str) -> Result<Token> {
323        take_until(1.., '.')
324            .map(|s: &str| Token::Name(s.to_owned()))
325            .context(StrContext::Label("name"))
326            .context(StrContext::Expected(StrContextValue::Description(
327                "name not to contain `.`s",
328            )))
329            .parse_next(input)
330    }
331
332    fn updown(input: &mut &str) -> Result<Token> {
333        alt((
334            "down".value(Token::UpDown(UpDown::Down)),
335            "undo".value(Token::DoUndo(DoUndo::Undo)),
336            "up".value(Token::UpDown(UpDown::Up)),
337            "do".value(Token::DoUndo(DoUndo::Do)),
338        ))
339        .context(StrContext::Label("updown"))
340        .context(StrContext::Expected(StrContextValue::StringLiteral("up")))
341        .context(StrContext::Expected(StrContextValue::StringLiteral("down")))
342        .context(StrContext::Expected(StrContextValue::StringLiteral("do")))
343        .context(StrContext::Expected(StrContextValue::StringLiteral("undo")))
344        .parse_next(input)
345    }
346
347    fn prefix(input: &mut &str) -> Result<Token> {
348        fn z_prefix<'i>(input: &mut &'i str) -> Result<&'i str> {
349            take_while(0.., |c| c == 'z' || c == 'Z').parse_next(input)
350        }
351
352        fn v_prefix<'i>(input: &mut &'i str) -> Result<&'i str> {
353            alt(('v', 'V')).take().parse_next(input)
354        }
355
356        (z_prefix, v_prefix)
357            .take()
358            .map(|s: &str| Token::Prefix(s.to_owned()))
359            .parse_next(input)
360    }
361
362    fn number(input: &mut &str) -> Result<Vec<Token>> {
363        (
364            alt((
365                datetime,
366                epoch_timestamp,
367                semver,
368                padded_number,
369                fail.context(StrContext::Label("number"))
370                    .context(StrContext::Expected(StrContextValue::Description(
371                        "datetime",
372                    )))
373                    .context(StrContext::Expected(StrContextValue::Description(
374                        "epoch timestamp",
375                    )))
376                    .context(StrContext::Expected(StrContextValue::Description(
377                        "padded number",
378                    )))
379                    .context(StrContext::Expected(StrContextValue::Description("semver"))),
380            )),
381            opt((
382                repeat(0.., sep).map(|t: Vec<_>| t),
383                alt((epoch_timestamp, random_number)),
384            )),
385        )
386            .map(|(t1, t2)| {
387                let mut tokens = vec![Some(t1)];
388
389                if let Some((s, t)) = t2 {
390                    s.into_iter().for_each(|s| tokens.push(Some(s)));
391                    tokens.push(Some(t));
392                }
393
394                tokens.into_iter().flatten().collect()
395            })
396            .parse_next(input)
397    }
398
399    fn dir_ident(input: &mut &str) -> Result<Segment> {
400        (
401            opt(prefix),
402            number,
403            opt((repeat(1.., sep).map(|t: Vec<_>| t), name)),
404        )
405            .map(|(prefix, number, name)| {
406                let mut children = vec![prefix];
407                number.into_iter().for_each(|s| children.push(Some(s)));
408
409                if let Some((sep, name)) = name {
410                    sep.into_iter().for_each(|s| children.push(Some(s)));
411                    children.push(Some(name));
412                }
413
414                let tokens = children.into_iter().flatten().collect();
415
416                Segment {
417                    kind: SegmentKind::Dir,
418                    tokens,
419                }
420            })
421            .parse_next(input)
422    }
423
424    fn file_ext(input: &mut &str) -> Result<Token> {
425        ".sql"
426            .value(Token::Extension)
427            .context(StrContext::Label("file ext"))
428            .context(StrContext::Expected(StrContextValue::StringLiteral(".sql")))
429            .parse_next(input)
430    }
431
432    fn file_nonident(input: &mut &str) -> Result<Segment> {
433        (updown, file_ext)
434            .map(|(updown, ext)| Segment {
435                kind: SegmentKind::File,
436                tokens: vec![updown, ext],
437            })
438            .parse_next(input)
439    }
440
441    fn file_ident(input: &mut &str) -> Result<Segment> {
442        (
443            opt(prefix),
444            number,
445            opt((repeat(0.., sep).map(|t: Vec<_>| t), name)),
446            opt((dot, updown)),
447            file_ext,
448        )
449            .map(|(prefix, number, name, updown, ext)| {
450                let mut children = vec![prefix];
451                number.into_iter().for_each(|s| children.push(Some(s)));
452
453                if let Some((sep, name)) = name {
454                    sep.into_iter().for_each(|s| children.push(Some(s)));
455                    children.push(Some(name));
456                }
457
458                if let Some((sep, updown)) = updown {
459                    children.push(Some(sep));
460                    children.push(Some(updown));
461                }
462
463                children.push(Some(ext));
464
465                let tokens = children.into_iter().flatten().collect();
466
467                Segment {
468                    kind: SegmentKind::File,
469                    tokens,
470                }
471            })
472            .parse_next(input)
473    }
474
475    fn path_sep<'i>(input: &mut &'i str) -> Result<&'i str> {
476        alt(('/', '\\')).take().parse_next(input)
477    }
478
479    fn path(input: &mut &str) -> Result<Vec<Segment>> {
480        alt((
481            (dir_ident, path_sep, file_nonident).map(|(dir, _sep, file)| vec![dir, file]),
482            file_ident.map(|file| vec![file]),
483        ))
484        .parse_next(input)
485    }
486
487    pub fn parse(input: &str) -> std::result::Result<PathTemplate, ParseError> {
488        let segments = path.parse(input).map_err(|e| ParseError {
489            message: e.inner().to_string(),
490            span: e.char_span(),
491            input: input.to_owned(),
492        })?;
493
494        Ok(PathTemplate { segments })
495    }
496}
497
498mod ast {
499    use std::{fmt, str::FromStr};
500
501    use anyhow::anyhow;
502    use chrono::Utc;
503
504    use super::parser::{self, ParseError};
505
506    #[derive(Debug, PartialEq)]
507    pub struct PathTemplate {
508        pub(crate) segments: Vec<Segment>,
509    }
510
511    impl PathTemplate {
512        pub fn parse(path: &str) -> Result<Self, ParseError> {
513            parser::parse(path)
514        }
515
516        pub fn includes_up_down(&self) -> bool {
517            self.segments.iter().any(|s| {
518                s.tokens
519                    .iter()
520                    .rev()
521                    .any(|t| matches!(t, Token::UpDown(_) | Token::DoUndo(_)))
522            })
523        }
524
525        pub fn with_up_down(self) -> Self {
526            let mut segments = self.segments;
527            if let Some(s) = segments.last_mut() {
528                let ext = s.tokens.pop().unwrap_or(Token::Extension);
529                if !matches!(
530                    s.tokens.last(),
531                    Some(Token::UpDown(_)) | Some(Token::DoUndo(_))
532                ) {
533                    s.tokens.push(Token::Dot);
534                    s.tokens.push(Token::UpDown(UpDown::Up));
535                }
536                s.tokens.push(ext);
537            }
538            Self { segments }
539        }
540
541        pub fn resolve(&self, data: &TemplateData) -> String {
542            super::resolver::Resolve::resolve(self, data)
543        }
544    }
545
546    impl Default for PathTemplate {
547        fn default() -> Self {
548            Self {
549                segments: vec![Segment {
550                    kind: SegmentKind::File,
551                    tokens: vec![
552                        Token::Timestamp(Timestamp::Epoch(EpochTimestamp::Second(0))),
553                        Token::Underscore,
554                        Token::Name("generated_migration".to_string()),
555                        Token::Dot,
556                        Token::UpDown(UpDown::Up),
557                        Token::Extension,
558                    ],
559                }],
560            }
561        }
562    }
563
564    #[derive(Debug, PartialEq)]
565    pub struct Segment {
566        pub kind: SegmentKind,
567        pub tokens: Vec<Token>,
568    }
569
570    #[derive(Debug, PartialEq)]
571    pub enum SegmentKind {
572        Dir,
573        File,
574    }
575
576    #[derive(Debug, Clone, Default, PartialEq)]
577    pub struct TemplateData {
578        pub timestamp: chrono::DateTime<Utc>,
579        pub name: String,
580        pub up_down: Option<UpDown>,
581        pub counter: Option<usize>,
582        pub random: Option<usize>,
583        pub semver: Option<Semver>,
584    }
585
586    #[derive(Debug, Clone, PartialEq)]
587    pub enum Token {
588        /// e.g. "V"
589        Prefix(String),
590        /// padded number e.g. 0001, 0002, etc.
591        PaddedNumber(PaddedNumber),
592        /// any sequence of numbers
593        RandomNumber(usize),
594        /// e.g. 0.1.0, 11.12.13, etc
595        Semver(Semver),
596        /// represents a date/time
597        Timestamp(Timestamp),
598        /// name of the migration
599        Name(String),
600        /// either ".up" or ".down"
601        UpDown(UpDown),
602        /// either ".do" or ".undo" (alias for UpDown)
603        DoUndo(DoUndo),
604        /// literal underscore ("_")
605        Underscore,
606        /// literal dot (".")
607        Dot,
608        /// literal dash ("-")
609        Dash,
610        /// file extension (e.g. ".sql")
611        Extension,
612    }
613
614    #[derive(Debug, Clone, PartialEq)]
615    pub struct PaddedNumber {
616        pub width: usize,
617        pub number: usize,
618    }
619
620    impl FromStr for PaddedNumber {
621        type Err = anyhow::Error;
622
623        fn from_str(s: &str) -> Result<Self, Self::Err> {
624            let width = s.len();
625            let number = s.parse::<usize>()?;
626
627            Ok(Self { width, number })
628        }
629    }
630
631    #[derive(Debug, Clone, PartialEq)]
632    pub enum UpDown {
633        Up,
634        Down,
635    }
636
637    impl FromStr for UpDown {
638        type Err = anyhow::Error;
639
640        fn from_str(s: &str) -> Result<Self, Self::Err> {
641            Ok(match s {
642                "up" => Self::Up,
643                "down" => Self::Down,
644                _ => return Err(anyhow!("invalid UP_DOWN token: {:?}", s)),
645            })
646        }
647    }
648
649    impl From<DoUndo> for UpDown {
650        fn from(value: DoUndo) -> Self {
651            match value {
652                DoUndo::Do => Self::Up,
653                DoUndo::Undo => Self::Down,
654            }
655        }
656    }
657
658    #[derive(Debug, Clone, PartialEq)]
659    pub enum DoUndo {
660        Do,
661        Undo,
662    }
663
664    impl FromStr for DoUndo {
665        type Err = anyhow::Error;
666
667        fn from_str(s: &str) -> Result<Self, Self::Err> {
668            Ok(match s {
669                "do" => Self::Do,
670                "undo" => Self::Undo,
671                _ => return Err(anyhow!("invalid DO_UNDO token: {:?}", s)),
672            })
673        }
674    }
675
676    #[derive(Debug, Clone, PartialEq)]
677    pub struct Semver {
678        major: u32,
679        minor: u32,
680        patch: u32,
681        widths: (usize, usize, usize),
682    }
683
684    impl Semver {
685        pub fn increment_minor(self) -> Self {
686            Self {
687                minor: self.minor + 1,
688                patch: 0,
689                ..self
690            }
691        }
692    }
693
694    impl fmt::Display for Semver {
695        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
696            let (w1, w2, w3) = self.widths;
697            write!(
698                f,
699                "{:0>w1$}.{:0>w2$}.{:0>w3$}",
700                self.major, self.minor, self.patch
701            )
702        }
703    }
704
705    impl FromStr for Semver {
706        type Err = anyhow::Error;
707
708        fn from_str(s: &str) -> Result<Self, Self::Err> {
709            let parts = s
710                .splitn(3, '.')
711                .map(|s| {
712                    let width = s.len();
713                    let num = s.parse::<u32>()?;
714                    Ok::<_, anyhow::Error>((width, num))
715                })
716                .collect::<Result<Vec<_>, _>>()?;
717
718            if parts.len() != 3 {
719                return Err(anyhow!("invalid semver: {s}"));
720            }
721
722            Ok(Self {
723                major: parts[0].1,
724                minor: parts[1].1,
725                patch: parts[2].1,
726                widths: (parts[0].0, parts[1].0, parts[2].0),
727            })
728        }
729    }
730
731    impl Default for Semver {
732        fn default() -> Self {
733            Self {
734                major: 0,
735                minor: 1,
736                patch: 0,
737                widths: (6, 6, 2),
738            }
739        }
740    }
741
742    #[derive(Debug, Clone, PartialEq)]
743    pub enum Timestamp {
744        Epoch(EpochTimestamp),
745        DateTime(DateTime),
746    }
747
748    impl TryFrom<Timestamp> for chrono::DateTime<Utc> {
749        type Error = anyhow::Error;
750
751        fn try_from(ts: Timestamp) -> Result<Self, Self::Error> {
752            Ok(match ts {
753                Timestamp::Epoch(ts) => match ts {
754                    EpochTimestamp::Nano(nsecs) => chrono::DateTime::from_timestamp_nanos(nsecs),
755                    EpochTimestamp::Micro(micros) => {
756                        chrono::DateTime::from_timestamp_micros(micros)
757                            .ok_or_else(|| anyhow!("invalid timestamp: {ts:?}"))?
758                    }
759                    EpochTimestamp::Milli(millis) => {
760                        chrono::DateTime::from_timestamp_millis(millis)
761                            .ok_or_else(|| anyhow!("invalid timestamp: {ts:?}"))?
762                    }
763                    EpochTimestamp::Second(secs) => chrono::DateTime::from_timestamp(secs, 0)
764                        .ok_or_else(|| anyhow!("invalid timestamp: {ts:?}"))?,
765                },
766                Timestamp::DateTime(dt) => {
767                    let datetime = chrono::NaiveDateTime::try_from(dt)?;
768                    chrono::DateTime::from_naive_utc_and_offset(datetime, *Utc::now().offset())
769                }
770            })
771        }
772    }
773
774    #[derive(Debug, Clone, PartialEq)]
775    pub enum EpochTimestamp {
776        /// seconds since Jan 1, 1970
777        Second(i64),
778        /// milliseconds since Jan 1, 1970
779        Milli(i64),
780        /// microseconds since Jan 1, 1970
781        Micro(i64),
782        /// nanoseconds since Jan 1, 1970
783        Nano(i64),
784    }
785
786    #[derive(Debug, Clone, PartialEq, Default)]
787    pub struct DateTime {
788        pub date: Date,
789        pub date_sep: Option<String>,
790        pub time: Option<Time>,
791    }
792
793    impl TryFrom<DateTime> for chrono::NaiveDateTime {
794        type Error = anyhow::Error;
795
796        fn try_from(dt: DateTime) -> Result<Self, Self::Error> {
797            let date = chrono::NaiveDate::from_ymd_opt(dt.date.year, dt.date.month, dt.date.day)
798                .ok_or_else(|| anyhow!("invalid datetime: {dt:?}"))?;
799            let time = dt.time.map(chrono::NaiveTime::try_from).transpose()?;
800            Ok(chrono::NaiveDateTime::new(date, time.unwrap_or_default()))
801        }
802    }
803
804    #[derive(Debug, Clone, PartialEq, Default)]
805    pub struct Date {
806        pub year: i32,
807        pub year_sep: Option<String>,
808        pub month: u32,
809        pub month_sep: Option<String>,
810        pub day: u32,
811    }
812
813    impl TryFrom<Date> for chrono::NaiveDate {
814        type Error = anyhow::Error;
815
816        fn try_from(d: Date) -> Result<Self, Self::Error> {
817            chrono::NaiveDate::from_ymd_opt(d.year, d.month, d.day)
818                .ok_or_else(|| anyhow!("invalid date: {d:?}"))
819        }
820    }
821
822    #[derive(Debug, Clone, PartialEq, Default)]
823    pub struct Time {
824        pub hour: u32,
825        pub hour_sep: Option<String>,
826        pub minute: u32,
827        pub minute_sep: Option<String>,
828        pub second: Option<u32>,
829        pub second_sep: Option<String>,
830        pub subsecond: Option<SubSecond>,
831    }
832
833    impl TryFrom<Time> for chrono::NaiveTime {
834        type Error = anyhow::Error;
835
836        fn try_from(t: Time) -> Result<Self, Self::Error> {
837            let Time {
838                hour,
839                minute: min,
840                second: sec,
841                subsecond,
842                ..
843            } = t;
844            let sec = sec.unwrap_or_default();
845            match subsecond {
846                Some(SubSecond::Milli(milli)) => {
847                    chrono::NaiveTime::from_hms_milli_opt(hour, min, sec, milli)
848                }
849                Some(SubSecond::Micro(micro)) => {
850                    chrono::NaiveTime::from_hms_micro_opt(hour, min, sec, micro)
851                }
852                Some(SubSecond::Nano(nano)) => {
853                    chrono::NaiveTime::from_hms_nano_opt(hour, min, sec, nano)
854                }
855                None => chrono::NaiveTime::from_hms_opt(hour, min, sec),
856            }
857            .ok_or_else(|| anyhow!("invalid time: {hour:02?}:{min:02?}:{sec:02}"))
858        }
859    }
860
861    #[derive(Debug, Clone, PartialEq)]
862    pub enum SubSecond {
863        /// SSS
864        Milli(u32),
865        /// SSSSSS
866        Micro(u32),
867        /// SSSSSSSSS
868        Nano(u32),
869    }
870}
871
872mod resolver {
873    use chrono::{Datelike, Timelike};
874
875    use super::ast::{
876        Date, DateTime, DoUndo, EpochTimestamp, PaddedNumber, PathTemplate, Segment, Semver,
877        SubSecond, TemplateData, Time, Timestamp, Token, UpDown,
878    };
879
880    pub trait Resolve {
881        fn resolve(&self, data: &TemplateData) -> String;
882    }
883
884    impl Resolve for PathTemplate {
885        fn resolve(&self, data: &TemplateData) -> String {
886            self.segments
887                .iter()
888                .map(|s| Resolve::resolve(s, data))
889                .collect()
890        }
891    }
892
893    impl Resolve for Segment {
894        fn resolve(&self, data: &TemplateData) -> String {
895            self.tokens
896                .iter()
897                .enumerate()
898                .map(|(i, t)| {
899                    let next = self.tokens.get(i + 1);
900                    // special case: when there's an UpDown token and we're not rendering it, also don't render the preceding Dot token.
901                    if data.up_down.is_none()
902                        && matches!(t, Token::Dot)
903                        && matches!(next, Some(Token::UpDown(_)))
904                    {
905                        String::new()
906                    } else {
907                        Resolve::resolve(t, data)
908                    }
909                })
910                .collect()
911        }
912    }
913
914    impl Resolve for Token {
915        fn resolve(&self, data: &TemplateData) -> String {
916            match self {
917                Token::Prefix(prefix) => prefix.clone(),
918                Token::PaddedNumber(padding) => Resolve::resolve(padding, data),
919                Token::RandomNumber(_) => {
920                    if let Some(num) = data.random {
921                        num.to_string()
922                    } else {
923                        data.timestamp.timestamp_micros().to_string()
924                    }
925                }
926                Token::Semver(v) => Resolve::resolve(v, data),
927                Token::Timestamp(ts) => Resolve::resolve(ts, data),
928                Token::Name(_) => data.name.clone(),
929                Token::UpDown(updown) => Resolve::resolve(updown, data),
930                Token::DoUndo(updown) => Resolve::resolve(updown, data),
931                Token::Underscore => "_".to_owned(),
932                Token::Dot => ".".to_owned(),
933                Token::Dash => "-".to_owned(),
934                Token::Extension => ".sql".to_owned(),
935            }
936        }
937    }
938
939    impl Resolve for PaddedNumber {
940        fn resolve(&self, data: &TemplateData) -> String {
941            let counter = data.counter.unwrap_or(self.number + 1);
942            format!("{:0>width$}", counter, width = self.width)
943        }
944    }
945
946    impl Resolve for Semver {
947        fn resolve(&self, data: &TemplateData) -> String {
948            let num = if let Some(num) = data.semver.clone() {
949                num
950            } else {
951                self.clone().increment_minor()
952            };
953            format!("{num}")
954        }
955    }
956
957    impl Resolve for Timestamp {
958        fn resolve(&self, data: &TemplateData) -> String {
959            match self {
960                Self::Epoch(ts) => Resolve::resolve(ts, data),
961                Self::DateTime(dt) => Resolve::resolve(dt, data),
962            }
963        }
964    }
965
966    impl Resolve for EpochTimestamp {
967        fn resolve(&self, data: &TemplateData) -> String {
968            let ts = data.timestamp;
969            match self {
970                Self::Second(_) => ts.timestamp(),
971                Self::Milli(_) => ts.timestamp_millis(),
972                Self::Micro(_) => ts.timestamp_micros(),
973                Self::Nano(_) => ts.timestamp_nanos_opt().unwrap_or(0),
974            }
975            .to_string()
976        }
977    }
978
979    impl Resolve for DateTime {
980        fn resolve(&self, data: &TemplateData) -> String {
981            Resolve::resolve(&self.date, data)
982                + self.date_sep.clone().unwrap_or_default().as_str()
983                + self
984                    .time
985                    .as_ref()
986                    .map(|t| Resolve::resolve(t, data))
987                    .unwrap_or("".to_owned())
988                    .as_str()
989        }
990    }
991
992    impl Resolve for Date {
993        fn resolve(&self, data: &TemplateData) -> String {
994            let ts = data.timestamp;
995            format!(
996                "{:02}{}{:02}{}{:02}",
997                ts.year(),
998                self.year_sep.clone().unwrap_or_default(),
999                ts.month(),
1000                self.month_sep.clone().unwrap_or_default(),
1001                ts.day()
1002            )
1003        }
1004    }
1005
1006    impl Resolve for Time {
1007        fn resolve(&self, data: &TemplateData) -> String {
1008            let ts = data.timestamp;
1009            format!(
1010                "{:02}{}{:02}{}{:02}{}{}",
1011                ts.hour(),
1012                self.hour_sep.clone().unwrap_or_default(),
1013                ts.minute(),
1014                self.minute_sep.clone().unwrap_or_default(),
1015                self.second
1016                    .map(|_| format!("{:02}", ts.second()))
1017                    .unwrap_or_default(),
1018                self.second_sep.clone().unwrap_or_default(),
1019                self.subsecond
1020                    .as_ref()
1021                    .map(|sss| Resolve::resolve(sss, data))
1022                    .unwrap_or_default(),
1023            )
1024        }
1025    }
1026
1027    impl Resolve for SubSecond {
1028        fn resolve(&self, data: &TemplateData) -> String {
1029            let ts = data.timestamp;
1030            match self {
1031                Self::Milli(_) => ts.timestamp_subsec_millis().to_string(),
1032                Self::Micro(_) => ts.timestamp_subsec_micros().to_string(),
1033                Self::Nano(_) => ts.timestamp_subsec_nanos().to_string(),
1034            }
1035        }
1036    }
1037
1038    impl Resolve for UpDown {
1039        fn resolve(&self, data: &TemplateData) -> String {
1040            match data.up_down {
1041                Some(UpDown::Up) => "up",
1042                Some(UpDown::Down) => "down",
1043                None => "",
1044            }
1045            .to_owned()
1046        }
1047    }
1048
1049    impl Resolve for DoUndo {
1050        fn resolve(&self, data: &TemplateData) -> String {
1051            match data.up_down {
1052                Some(UpDown::Up) => "do",
1053                Some(UpDown::Down) => "undo",
1054                None => "",
1055            }
1056            .to_owned()
1057        }
1058    }
1059}
1060
1061#[cfg(test)]
1062mod tests {
1063    use anyhow::Context;
1064    use chrono::Utc;
1065
1066    use super::ast::{PathTemplate, Semver, TemplateData, Token, UpDown};
1067
1068    fn data(tmpl: &PathTemplate) -> TemplateData {
1069        let mut data = TemplateData::default();
1070        let mut timestamp = data.timestamp;
1071        tmpl.segments
1072            .iter()
1073            .flat_map(|s| &s.tokens)
1074            .for_each(|t| {
1075                match t {
1076                    Token::Timestamp(ts) => timestamp = ts.clone().try_into().unwrap(),
1077                    Token::Name(name) => data.name = name.clone(),
1078                    Token::PaddedNumber(padding) => data.counter = Some(padding.number),
1079                    Token::RandomNumber(rand) => data.random = Some(*rand),
1080                    Token::Semver(semver) => data.semver = Some(semver.clone()),
1081                    Token::UpDown(updown) => {
1082                        data.up_down = Some(updown.clone());
1083                    }
1084                    Token::DoUndo(doundo) => {
1085                        data.up_down = Some(doundo.clone().into());
1086                    }
1087                    // the rest of the data is used directly
1088                    _ => {}
1089                };
1090            });
1091        data.timestamp = timestamp;
1092        data
1093    }
1094
1095    #[test]
1096    fn test_parse_resolve() {
1097        vec![
1098            "1741141452_generated_migration.down.sql",
1099            "000522_add_users_full_name.undo.sql",
1100            "000522_create_users.do.sql",
1101            "000522_inital_schema.sql",
1102            "002_create_users_table.sql",
1103            "006_create_categories_table.sql",
1104            "010_add_foreign_key_to_posts.sql",
1105            "014_add_roles_to_users.sql",
1106            "017_create_logs_table.sql",
1107            "020_add_soft_delete_to_users.sql",
1108            "1007728000000000000_inital_schema.sql",
1109            "1007728000000000_inital_schema.sql",
1110            "1007728000000_inital_schema.sql",
1111            "1007728000_inital_schema.sql",
1112            "1036400000000000000_create_users.do.sql",
1113            "1036400000000000_create_users.sql",
1114            "1036400000000_create_users.sql",
1115            "1065072000000000000_add_users_full_name.undo.sql",
1116            "1704067200123_add_users_full_name.sql",
1117            "1704067200_add_users_full_name.sql",
1118            "1798675200123456_add_users_full_name.sql",
1119            "1893283200_create_users.sql",
1120            "2001-12-07.07-26-400_inital_schema.sql",
1121            "2002-11-04.03-53-200_create_users.up.sql",
1122            "2003-10-02.01-20-000_add_users_full_name.down.sql",
1123            "2023-01-04_add_comments_table.sql",
1124            "2023-01-12_add_tags_to_posts.sql",
1125            "2023-01-18_add_timestamp_to_posts.sql",
1126            "20230101_initial_setup.sql",
1127            "20230108_drop_comments_table.sql",
1128            "20230115_create_settings_table.sql",
1129            "v1_create_posts_table.sql",
1130            "v200112070726400_inital_schema.sql",
1131            "v200211040353200_create_users.up.sql",
1132            "v200211040353200_create_users.up.sql",
1133            "v20201231190000123456_add_users_full_name.down.sql",
1134            "v2_create_tags_table.sql",
1135            "v2.2.2_create_tags_table.sql",
1136            "v11.12.13_create_tags_table.sql",
1137            "v88.99.00_create_tags_table.sql",
1138            "11.12.13_create_tags_table.sql",
1139            "0011.0012.0013_create_tags_table.sql",
1140            "zv2234234203984209384_oops_we_ran_out_of_digits.sql",
1141            // dirs
1142            "017_create_logs_table/do.sql",
1143            "1704067200_add_users_full_name/up.sql",
1144            "2003-10-02.01-20-000_add_users_full_name/down.sql",
1145            "v1_create_posts_table/up.sql",
1146            "v20201231190000123456_add_users_full_name/down.sql",
1147            "v0.1.0_add_users_full_name/down.sql",
1148            "v11.12.13_add_users_full_name/down.sql",
1149            "11.12.13_add_users_full_name/down.sql",
1150            "1011.0012.0013_add_users_full_name/down.sql",
1151        ]
1152        .into_iter()
1153        .enumerate()
1154        .for_each(|(i, input)| {
1155            eprintln!("{input:?}");
1156            let template = super::parser::parse(input)
1157                .context(format!("test case {i:02}"))
1158                .unwrap_or_else(|_| panic!("{input} should parse"));
1159            let data = data(&template);
1160            let template = template.with_up_down();
1161            let out = template.resolve(&data);
1162            assert_eq!(
1163                out, input,
1164                "template should resolve to input\n{template:?}\n{data:?}"
1165            );
1166
1167            vec![
1168                |data: TemplateData| TemplateData {
1169                    name: "some_other_name".to_owned(),
1170                    ..data
1171                },
1172                |data: TemplateData| TemplateData {
1173                    timestamp: Utc::now(),
1174                    counter: Some(data.counter.map_or(1, |c| c + 1)),
1175                    random: Some(data.random.map_or(1, |r| r + 1)),
1176                    semver: Some(
1177                        data.semver
1178                            .map_or(Semver::default(), |s| s.increment_minor()),
1179                    ),
1180                    ..data
1181                },
1182                |data: TemplateData| TemplateData {
1183                    up_down: match data.up_down {
1184                        Some(UpDown::Up) => Some(UpDown::Down),
1185                        Some(UpDown::Down) => Some(UpDown::Up),
1186                        None => Some(UpDown::Up),
1187                    },
1188                    ..data
1189                },
1190            ]
1191            .into_iter()
1192            .for_each(|f| {
1193                let data = f(data.clone());
1194                let out = template.resolve(&data);
1195                assert_ne!(
1196                    out, input,
1197                    "template should adapt based on input data\n{template:?}\n{data:?}"
1198                );
1199            });
1200        });
1201    }
1202}