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**.
|
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
|
## Example Requests and Responses
|
||||||
|
|||||||
@ -1,5 +1,10 @@
|
|||||||
package dsv.su.apimposter.service;
|
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.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -32,6 +37,11 @@ public class MockService {
|
|||||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||||
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{([\\w\\.]+)(?::(int|float|bool))?}");
|
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,
|
* Initializes the mock service with a reference to the file loader,
|
||||||
* which holds the live endpoint and global placeholder data.
|
* which holds the live endpoint and global placeholder data.
|
||||||
@ -142,6 +152,12 @@ public class MockService {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
} else if (body instanceof String str) {
|
} 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);
|
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 the string is a single placeholder (e.g. "{id:int}" or "{id}"), resolve and return its value
|
||||||
if (matcher.matches()) {
|
if (matcher.matches()) {
|
||||||
@ -167,6 +183,169 @@ public class MockService {
|
|||||||
return body;
|
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.
|
* Attempts to convert a raw string into a typed value if a type hint is provided.
|
||||||
* Supported types:
|
* Supported types:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user