1pub 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>| ()) .take()
100 .parse_to::<Semver>()
101 .map(Token::Semver)
102 .parse_next(input)
103 }
104
105 const MIN_DATE: NaiveDate = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
107 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 Prefix(String),
590 PaddedNumber(PaddedNumber),
592 RandomNumber(usize),
594 Semver(Semver),
596 Timestamp(Timestamp),
598 Name(String),
600 UpDown(UpDown),
602 DoUndo(DoUndo),
604 Underscore,
606 Dot,
608 Dash,
610 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 Second(i64),
778 Milli(i64),
780 Micro(i64),
782 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 Milli(u32),
865 Micro(u32),
867 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 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 _ => {}
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 "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}