Add index-support, type hints and improve documentation #4
149
README.md
149
README.md
@ -10,7 +10,7 @@ APImposter is an easy-to-use service built with Spring Boot, enabling quick mock
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Key Features
|
## Features
|
||||||
|
|
||||||
- **Structured Mock Data:** Organize your mock data in YAML files.
|
- **Structured Mock Data:** Organize your mock data in YAML files.
|
||||||
- **Automatic URL Mapping:** Automatically generates URL structure based on your folder hierarchy and YAML file names.
|
- **Automatic URL Mapping:** Automatically generates URL structure based on your folder hierarchy and YAML file names.
|
||||||
@ -18,80 +18,131 @@ APImposter is an easy-to-use service built with Spring Boot, enabling quick mock
|
|||||||
- **Dynamic Responses and Global Placeholders:** Supports dynamic values in responses based on URL parameters and you can define global placeholders to reuse values across multiple responses and systems.
|
- **Dynamic Responses and Global Placeholders:** Supports dynamic values in responses based on URL parameters and you can define global placeholders to reuse values across multiple responses and systems.
|
||||||
- **Conditional Responses:** Return different mock responses based on request data (headers, query, body, or path).
|
- **Conditional Responses:** Return different mock responses based on request data (headers, query, body, or path).
|
||||||
- **Swagger UI:** Automatically generated Swagger UI from YAML files.
|
- **Swagger UI:** Automatically generated Swagger UI from YAML files.
|
||||||
- **Quick Setup:** Easy installation and configuration.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mocks Structure
|
## Mocks Structure
|
||||||
|
|
||||||
YAML defined mocks are stored in a directory defined in `application.properties`. You can organize them in nested folders like this:
|
Mocks are defined in YAML files and stored in a directory specified via `application.properties`. You can organize them in nested folders:
|
||||||
|
|
||||||
```
|
```
|
||||||
/mocks/
|
/mocks/
|
||||||
├── globals.yaml
|
├── globals.yaml
|
||||||
├── system1/
|
├── system1/
|
||||||
│ ├── users.yaml
|
│ ├── users.yaml
|
||||||
│ └── program.yaml
|
│ └── index.yaml
|
||||||
└── another-system/
|
└── another-system/
|
||||||
├── endpoint1.yaml
|
├── endpoint1.yaml
|
||||||
└── subdir/
|
└── subdir/
|
||||||
└── endpoint2.yaml
|
├── endpoint2.yaml
|
||||||
|
└── index.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Example YAML files:
|
### URL Mapping
|
||||||
|
|
||||||
|
- `/system1/users.yaml` defines mocks under `/system1/users/...`
|
||||||
|
- `/system1/index.yaml` defines mocks under`/system1/...`)
|
||||||
|
- Multiple endpoints can be grouped in one file
|
||||||
|
- Folders in the YAML file structure become part of the URL path automatically.
|
||||||
|
|
||||||
|
### Grouped endpoints in `users.yaml`
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
# globals.yaml
|
# mocks/system1/users.yaml
|
||||||
|
- method: GET
|
||||||
|
path: "/"
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
body:
|
||||||
|
users:
|
||||||
|
- id: 1
|
||||||
|
name: "Alice"
|
||||||
|
- id: 2
|
||||||
|
name: "Bob"
|
||||||
|
|
||||||
|
- method: GET
|
||||||
|
path: "/{id}"
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
body:
|
||||||
|
id: "{id:int}"
|
||||||
|
name: "User {id}"
|
||||||
|
|
||||||
|
- method: POST
|
||||||
|
path: "/"
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
body:
|
||||||
|
message: "User created"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Base-level grouping via `index.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# mocks/system1/index.yaml
|
||||||
|
- method: GET
|
||||||
|
path: "/"
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
body:
|
||||||
|
message: "Welcome to system1"
|
||||||
|
|
||||||
|
- method: GET
|
||||||
|
path: "/info"
|
||||||
|
response:
|
||||||
|
status: 200
|
||||||
|
body:
|
||||||
|
name: "System One"
|
||||||
|
version: "1.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Placeholder Type Hints
|
||||||
|
|
||||||
|
You can include optional type hints in placeholders to ensure JSON values are rendered with correct types.
|
||||||
|
|
||||||
|
#### Supported types
|
||||||
|
|
||||||
|
| Type | Placeholder | Output |
|
||||||
|
|---------|------------------|-----------------|
|
||||||
|
| string | `{name}` | `"Alice"` |
|
||||||
|
| int | `{id:int}` | `42` *(number)* |
|
||||||
|
| float | `{price:float}` | `99.95` |
|
||||||
|
| boolean | `{enabled:bool}` | `true` |
|
||||||
|
|
||||||
|
If parsing fails (e.g. `abc` for an `int`), APImposter **falls back to the original string**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example Requests and Responses
|
||||||
|
|
||||||
|
| YAML file | Request | Response Example |
|
||||||
|
|------------------------------------|----------------------------------|----------------------------------------------|
|
||||||
|
| `system1/users.yaml` | `GET /system1/users` | `{ "users": [{ "id": 1 }, { "id": 2 }] }` |
|
||||||
|
| `system1/users.yaml` | `GET /system1/users/42` | `{ "id": 42, "name": "User 42" }` |
|
||||||
|
| `system1/users.yaml` | `POST /system1/users` | `{ "message": "User created" }` |
|
||||||
|
| `system1/index.yaml` | `GET /system1` | `{ "message": "Welcome to system1" }` |
|
||||||
|
| `system1/index.yaml` | `GET /system1/info` | `{ "name": "System One", "version": "1.0" }` |
|
||||||
|
| `another-system/subdir/index.yaml` | `GET /another-system/subdir/...` | *(Defined response in file)* |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Globals
|
||||||
|
|
||||||
|
Global values can be defined in a dedicated file `globals.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# mocks/globals.yaml
|
||||||
globals:
|
globals:
|
||||||
currentUserId: "user-123"
|
currentUserId: "user-123"
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml
|
You can reference global values using `{globals.key}` or `{globals.key:type}` in responses.
|
||||||
# system1/users.yaml
|
|
||||||
- method: "GET"
|
|
||||||
path: "/{id}"
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body:
|
|
||||||
id: "{id}"
|
|
||||||
name: "Example User"
|
|
||||||
```
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# system1/program.yaml
|
|
||||||
- method: "GET"
|
|
||||||
path: "/"
|
|
||||||
delay: 500
|
|
||||||
response:
|
|
||||||
status: 200
|
|
||||||
headers:
|
|
||||||
Content-Type: "application/json"
|
|
||||||
body:
|
|
||||||
programId: 12345
|
|
||||||
globalUserId: "{globals.currentUserId}"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
## Example Requests and Responses
|
|
||||||
|
|
||||||
| YAML file | Request | Response |
|
|
||||||
| --------------------------------------- |--------------------------------------------|-------------------------------------------|
|
|
||||||
| `mocks/system1/users.yaml` | `GET /<prefix>/system1/users/123` | `{ "id": "123", "name": "Example User" }` |
|
|
||||||
| `mocks/system1/program.yaml` | `GET /<prefix>/system1/program` | `{ "programId": 12345, "globalUserId": "user-123" }` |
|
|
||||||
| `mocks/another-system/subdir/endpoint2` | `GET /<prefix>/another-system/subdir/endpoint2/...` | *(defined response JSON)* |
|
|
||||||
|
|
||||||
Dynamic placeholders in responses (`{id}`) and global placeholders (`{globals.currentUserId}`) are automatically replaced with actual URL parameters or globally defined values.
|
|
||||||
|
|
||||||
Folders in the YAML file structure become part of the URL path automatically.
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Conditional Responses
|
## Conditional Responses
|
||||||
|
|
||||||
APImposter supports **conditional responses**, allowing different mock responses to be returned **based on request values** — like query parameters, headers, path variables, or body fields.
|
APImposter supports **conditional responses**, allowing different mock responses to be returned **based on request values** - query parameters, headers, path variables, or body fields.
|
||||||
|
|
||||||
This is useful for simulating realistic API behavior, such as authentication, filtering, or alternate success/error outcomes.
|
This is useful for simulating realistic API behavior, such as authentication, filtering, or alternate success/error outcomes.
|
||||||
|
|
||||||
@ -184,7 +235,7 @@ This is useful for simulating realistic API behavior, such as authentication, fi
|
|||||||
body:
|
body:
|
||||||
products:
|
products:
|
||||||
- id: "t1"
|
- id: "t1"
|
||||||
name: "Mechanical Keyboard"
|
name: "Keyboard"
|
||||||
response:
|
response:
|
||||||
status: 200
|
status: 200
|
||||||
headers: { Content-Type: "application/json" }
|
headers: { Content-Type: "application/json" }
|
||||||
|
|||||||
@ -46,9 +46,24 @@ public class MockFileParser {
|
|||||||
*/
|
*/
|
||||||
public void parseFile(Path file, String relativePath, List<MockEndpoint> endpoints, MockValidator validator) {
|
public void parseFile(Path file, String relativePath, List<MockEndpoint> endpoints, MockValidator validator) {
|
||||||
String basePath = "/" + relativePath;
|
String basePath = "/" + relativePath;
|
||||||
|
if (basePath.endsWith("/index")) {
|
||||||
|
basePath = basePath.substring(0, basePath.length() - "/index".length());
|
||||||
|
}
|
||||||
|
|
||||||
try (InputStream input = Files.newInputStream(file)) {
|
try (InputStream input = Files.newInputStream(file)) {
|
||||||
List<?> rawList = yaml.load(input);
|
Object root = yaml.load(input);
|
||||||
|
|
||||||
|
// Handle empty file
|
||||||
|
if (root == null) {
|
||||||
|
LOGGER.warn("⚠️ Skipping empty YAML file: {}", file);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the root YAML object is a list
|
||||||
|
if (!(root instanceof List<?> rawList)) {
|
||||||
|
LOGGER.error("❌ Expected a YAML list in file: {} but got {}", file, root.getClass().getSimpleName());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (Object obj : rawList) {
|
for (Object obj : rawList) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
|
||||||
import dsv.su.apimposter.model.Condition;
|
import dsv.su.apimposter.model.Condition;
|
||||||
import dsv.su.apimposter.model.ConditionalResponse;
|
import dsv.su.apimposter.model.ConditionalResponse;
|
||||||
@ -28,6 +30,7 @@ public class MockService {
|
|||||||
|
|
||||||
private final MockFileLoader loader;
|
private final MockFileLoader loader;
|
||||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||||
|
private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{([\\w\\.]+)(?::(int|float|bool))?}");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the mock service with a reference to the file loader,
|
* Initializes the mock service with a reference to the file loader,
|
||||||
@ -113,7 +116,12 @@ public class MockService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively replaces placeholders in the mock response body with actual values
|
* Recursively replaces placeholders in the mock response body with actual values
|
||||||
* from path variables and global placeholders.
|
* Supports simple and typed placeholders:
|
||||||
|
* - {@code {id}} - from path variables
|
||||||
|
* - {@code {id:int}} - from path variables, converted to int
|
||||||
|
* - {@code {globals.env}} - from globals
|
||||||
|
* - {@code {globals.port:int}} - from globals, converted to int
|
||||||
|
* If type conversion fails (e.g., {@code {port:int}} but value is "abc"), the raw string is used.
|
||||||
* @param body The response body template.
|
* @param body The response body template.
|
||||||
* @param pathVariables Extracted values from the request URL path.
|
* @param pathVariables Extracted values from the request URL path.
|
||||||
* @return Response body with all placeholders replaced.
|
* @return Response body with all placeholders replaced.
|
||||||
@ -134,20 +142,74 @@ public class MockService {
|
|||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
} else if (body instanceof String str) {
|
} else if (body instanceof String str) {
|
||||||
for (var entry : pathVariables.entrySet()) {
|
Matcher matcher = PLACEHOLDER_PATTERN.matcher(str);
|
||||||
str = str.replace("{" + entry.getKey() + "}", entry.getValue());
|
// If the string is a single placeholder (e.g. "{id:int}" or "{id}"), resolve and return its value
|
||||||
|
if (matcher.matches()) {
|
||||||
|
String key = matcher.group(1);
|
||||||
|
String type = matcher.group(2);
|
||||||
|
String raw = resolveValue(key, pathVariables, globals);
|
||||||
|
return convertWithType(raw, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var entry : globals.entrySet()) {
|
// Otherwise: perform inline replacement for all placeholders in the string
|
||||||
str = str.replace("{globals." + entry.getKey() + "}", entry.getValue().toString());
|
StringBuilder sb = new StringBuilder();
|
||||||
|
while (matcher.find()) {
|
||||||
|
String key = matcher.group(1);
|
||||||
|
String type = matcher.group(2);
|
||||||
|
String raw = resolveValue(key, pathVariables, globals);
|
||||||
|
Object replacement = convertWithType(raw, type);
|
||||||
|
matcher.appendReplacement(sb, Matcher.quoteReplacement(String.valueOf(replacement)));
|
||||||
}
|
}
|
||||||
|
matcher.appendTail(sb);
|
||||||
return str;
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to convert a raw string into a typed value if a type hint is provided.
|
||||||
|
* Supported types:
|
||||||
|
* - {@code int} - Integer.parseInt
|
||||||
|
* - {@code float} - Double.parseDouble
|
||||||
|
* - {@code bool} - true if "true", "1", "yes", "on" (case-insensitive)
|
||||||
|
* @param raw the string value to convert
|
||||||
|
* @param type the optional type hint
|
||||||
|
* @return the converted value, or the raw string if conversion fails or type is unknown
|
||||||
|
*/
|
||||||
|
private Object convertWithType(String raw, String type) {
|
||||||
|
if (type == null) return raw;
|
||||||
|
try {
|
||||||
|
return switch (type) {
|
||||||
|
case "int" -> Integer.parseInt(raw);
|
||||||
|
case "float" -> Double.parseDouble(raw);
|
||||||
|
case "bool" -> Boolean.parseBoolean(raw.toLowerCase());
|
||||||
|
default -> raw;
|
||||||
|
};
|
||||||
|
} catch (Exception e) {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a placeholder value from path variables or globals.
|
||||||
|
* - If the key starts with "globals.", the value is fetched from globals.
|
||||||
|
* - Otherwise, it's fetched from the pathVariables map.
|
||||||
|
* @param key the raw key name (e.g., "id", "globals.env")
|
||||||
|
* @param pathVariables path values extracted from the URI
|
||||||
|
* @param globals map of global variables
|
||||||
|
* @return the resolved string value, or empty string if not found
|
||||||
|
*/
|
||||||
|
private String resolveValue(String key, Map<String, String> pathVariables, Map<String, Object> globals) {
|
||||||
|
if (key.startsWith("globals.")) {
|
||||||
|
String globalKey = key.substring("globals.".length());
|
||||||
|
Object globalValue = globals.get(globalKey);
|
||||||
|
return globalValue != null ? String.valueOf(globalValue) : "";
|
||||||
|
} else {
|
||||||
|
return pathVariables.getOrDefault(key, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes a Java object into a JSON string.
|
* Serializes a Java object into a JSON string.
|
||||||
* If serialization fails, returns an error wrapper instead.
|
* If serialization fails, returns an error wrapper instead.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user