Add relative time and date #6

Merged
stne3960 merged 5 commits from relative_dates into main 2025-11-03 23:38:56 +01:00
2 changed files with 239 additions and 0 deletions
Showing only changes of commit d18914e699 - Show all commits

View File

@ -111,6 +111,66 @@ You can include optional type hints in placeholders to ensure JSON values are re
If parsing fails (e.g. `abc` for an `int`), APImposter **falls back to the original string**.
### Relative Date/Time Placeholders
APImposter supports dynamic date and time placeholders that are calculated relative to the current time. This is perfect for testing calendar applications, scheduling systems, or any time-sensitive mock data.
#### Supported formats
| Format | Example | Description |
|--------|---------|-------------|
| `{datetime+/-offset}` | `{datetime+2d4h30m}` | DateTime with flexible time offset |
| `{datetime+/-Nd@HH:MM}` | `{datetime+1d@13:30}` | DateTime N days from now at specific time |
| `{date+/-Nd}` | `{date-7d}` | Date N days from now |
#### Format specifiers (optional)
Add a format specifier after a colon to control the output format:
| Specifier | Description | Example Output |
|-----------|-------------|----------------|
| `iso` (default) | ISO standard format | `2025-09-03T15:30:00` |
| `rfc3339` | RFC 3339 with UTC timezone | `2025-09-03T15:30:00Z` |
| Custom pattern | Any valid DateTimeFormatter pattern | `Sep 03, 2025` |
#### Examples
```yaml
# Basic relative dates (ISO format)
- method: GET
path: "/events"
response:
body:
todayMeeting: "{datetime+0d@08:00}" # Today at 8:00 AM
tomorrowLunch: "{datetime+1d@13:00}" # Tomorrow at 1:00 PM
nextWeek: "{date+7d}" # Date 7 days from now
pastEvent: "{datetime-2d4h30m}" # 2 days, 4h, 30m ago
yesterday: "{date-1d}" # Yesterday's date
# Different output formats
- method: GET
path: "/calendar"
response:
body:
# RFC3339 format
eventTimeRfc: "{datetime+1d@15:30:rfc3339}" # "2025-09-03T15:30:00Z"
# Custom formats
eventDate: "{date+1d:MMM dd, yyyy}" # "Sep 03, 2025"
timeOnly: "{datetime+0d@14:30:HH:mm}" # "14:30"
shortDate: "{date+7d:MM/dd/yy}" # "09/09/25"
```
#### Time components for offset format
- `d` = days
- `h` = hours
- `m` = minutes
All components are optional and can be combined: `{datetime+1d2h}`, `{datetime+30m}`, `{datetime-4h15m}`
**Note:** If a custom format is invalid, the system automatically falls back to ISO format to prevent errors.
---
## Example Requests and Responses

View File

@ -1,5 +1,10 @@
package dsv.su.apimposter.service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
@ -32,6 +37,11 @@ public class MockService {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{([\\w\\.]+)(?::(int|float|bool))?}");
// Patterns for relative date/time placeholders with optional format specifier
private static final Pattern DATETIME_OFFSET_PATTERN = Pattern.compile("\\{datetime([+-])(\\d+d)?(\\d+h)?(\\d+m)?(?::(\\w+|[^}]+))?}");
private static final Pattern DATETIME_AT_TIME_PATTERN = Pattern.compile("\\{datetime([+-])(\\d+)d@(\\d{1,2}:\\d{2})(?::(\\w+|[^}]+))?}");
private static final Pattern DATE_OFFSET_PATTERN = Pattern.compile("\\{date([+-])(\\d+)d(?::(\\w+|[^}]+))?}");
/**
* Initializes the mock service with a reference to the file loader,
* which holds the live endpoint and global placeholder data.
@ -142,6 +152,12 @@ public class MockService {
.toList();
} else if (body instanceof String str) {
// Check for relative date/time placeholders first
String dateTimeResult = processDateTimePlaceholders(str);
if (!dateTimeResult.equals(str)) {
return dateTimeResult; // Date/time placeholder was processed
}
Matcher matcher = PLACEHOLDER_PATTERN.matcher(str);
// If the string is a single placeholder (e.g. "{id:int}" or "{id}"), resolve and return its value
if (matcher.matches()) {
@ -167,6 +183,169 @@ public class MockService {
return body;
}
/**
* Processes relative date/time placeholders in a string.
* Supports three formats with optional format specifiers:
* - {datetime+2d4h30m[:format]} - datetime now +/- time offset
* - {datetime+2d@13:00[:format]} - datetime now +/- days at specific time
* - {date-4d[:format]} - date now +/- days
*
* Format specifiers:
* - iso (default) - ISO standard format
* - rfc3339 - RFC 3339 format with UTC timezone
* - custom pattern - Valid DateTimeFormatter pattern
*
* Examples:
* - {datetime+1d} - "2025-09-03T14:30:00"
* - {datetime+1d:rfc3339} - "2025-09-03T14:30:00Z"
* - {date+1d:MMM dd, yyyy} - "Sep 03, 2025"
*
* @param input the input string that may contain date/time placeholders
* @return the string with date/time placeholders replaced, or original string if no placeholders found
*/
private String processDateTimePlaceholders(String input) {
String result = input;
// Process datetime with offset (e.g., {datetime+2d4h30m} or {datetime+2d4h30m:rfc3339})
Matcher dateTimeOffsetMatcher = DATETIME_OFFSET_PATTERN.matcher(result);
if (dateTimeOffsetMatcher.find()) {
String sign = dateTimeOffsetMatcher.group(1);
String daysStr = dateTimeOffsetMatcher.group(2);
String hoursStr = dateTimeOffsetMatcher.group(3);
String minutesStr = dateTimeOffsetMatcher.group(4);
String format = dateTimeOffsetMatcher.group(5);
LocalDateTime now = LocalDateTime.now();
LocalDateTime target = calculateDateTimeWithOffset(now, sign, daysStr, hoursStr, minutesStr);
String replacement = formatDateTime(target, format);
result = dateTimeOffsetMatcher.replaceFirst(replacement);
}
// Process datetime with days at specific time (e.g., {datetime+2d@13:00} or {datetime+2d@13:00:rfc3339})
Matcher dateTimeAtTimeMatcher = DATETIME_AT_TIME_PATTERN.matcher(result);
if (dateTimeAtTimeMatcher.find()) {
String sign = dateTimeAtTimeMatcher.group(1);
int days = Integer.parseInt(dateTimeAtTimeMatcher.group(2));
String timeStr = dateTimeAtTimeMatcher.group(3);
String format = dateTimeAtTimeMatcher.group(4);
LocalDate targetDate = LocalDate.now();
if ("+".equals(sign)) {
targetDate = targetDate.plusDays(days);
} else {
targetDate = targetDate.minusDays(days);
}
LocalTime time = LocalTime.parse(timeStr);
LocalDateTime target = LocalDateTime.of(targetDate, time);
String replacement = formatDateTime(target, format);
result = dateTimeAtTimeMatcher.replaceFirst(replacement);
}
// Process date with offset (e.g., {date-4d} or {date-4d:MMM dd, yyyy})
Matcher dateOffsetMatcher = DATE_OFFSET_PATTERN.matcher(result);
if (dateOffsetMatcher.find()) {
String sign = dateOffsetMatcher.group(1);
int days = Integer.parseInt(dateOffsetMatcher.group(2));
String format = dateOffsetMatcher.group(3);
LocalDate target = LocalDate.now();
if ("+".equals(sign)) {
target = target.plusDays(days);
} else {
target = target.minusDays(days);
}
String replacement = formatDate(target, format);
result = dateOffsetMatcher.replaceFirst(replacement);
}
return result;
}
/**
* Calculates a LocalDateTime with the given offset components.
* @param base the base datetime to start from
* @param sign "+" or "-" to indicate direction
* @param daysStr days component (e.g., "2d")
* @param hoursStr hours component (e.g., "4h")
* @param minutesStr minutes component (e.g., "30m")
* @return the calculated LocalDateTime
*/
private LocalDateTime calculateDateTimeWithOffset(LocalDateTime base, String sign, String daysStr, String hoursStr, String minutesStr) {
LocalDateTime result = base;
int multiplier = "+".equals(sign) ? 1 : -1;
if (daysStr != null) {
int days = Integer.parseInt(daysStr.replace("d", ""));
result = result.plusDays(days * multiplier);
}
if (hoursStr != null) {
int hours = Integer.parseInt(hoursStr.replace("h", ""));
result = result.plusHours(hours * multiplier);
}
if (minutesStr != null) {
int minutes = Integer.parseInt(minutesStr.replace("m", ""));
result = result.plusMinutes(minutes * multiplier);
}
return result;
}
/**
* Formats a LocalDateTime according to the specified format.
* Supported formats:
* - null or "iso" - ISO_LOCAL_DATE_TIME (default)
* - "rfc3339" - RFC 3339 format with UTC timezone
* - custom pattern - Any valid DateTimeFormatter pattern
* @param dateTime the LocalDateTime to format
* @param format the format specifier
* @return formatted date/time string
*/
private String formatDateTime(LocalDateTime dateTime, String format) {
if (format == null || "iso".equals(format)) {
return dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
try {
return switch (format) {
case "rfc3339" -> dateTime.atZone(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT);
default -> dateTime.format(DateTimeFormatter.ofPattern(format));
};
} catch (Exception e) {
// If format is invalid, fallback to ISO format
return dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
}
/**
* Formats a LocalDate according to the specified format.
* Supported formats:
* - null or "iso" - ISO_LOCAL_DATE (default)
* - "rfc3339" - ISO_LOCAL_DATE (same as ISO for date-only)
* - custom pattern - Any valid DateTimeFormatter pattern
* @param date the LocalDate to format
* @param format the format specifier
* @return formatted date string
*/
private String formatDate(LocalDate date, String format) {
if (format == null || "iso".equals(format) || "rfc3339".equals(format)) {
return date.format(DateTimeFormatter.ISO_LOCAL_DATE);
}
try {
return date.format(DateTimeFormatter.ofPattern(format));
} catch (Exception e) {
// If format is invalid, fallback to ISO format
return date.format(DateTimeFormatter.ISO_LOCAL_DATE);
}
}
/**
* Attempts to convert a raw string into a typed value if a type hint is provided.
* Supported types: