Add relative time and date #6
60
README.md
60
README.md
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user