From 443fd93b5eb30078154acb9488ba484afe9f8da0 Mon Sep 17 00:00:00 2001 From: nenzen Date: Fri, 30 May 2025 11:19:02 +0200 Subject: [PATCH 1/3] Handle index.yaml and improve logging --- .../dsv/su/apimposter/io/MockFileParser.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/main/java/dsv/su/apimposter/io/MockFileParser.java b/src/main/java/dsv/su/apimposter/io/MockFileParser.java index 65a3035..a9e638e 100644 --- a/src/main/java/dsv/su/apimposter/io/MockFileParser.java +++ b/src/main/java/dsv/su/apimposter/io/MockFileParser.java @@ -46,9 +46,24 @@ public class MockFileParser { */ public void parseFile(Path file, String relativePath, List endpoints, MockValidator validator) { String basePath = "/" + relativePath; + if (basePath.endsWith("/index")) { + basePath = basePath.substring(0, basePath.length() - "/index".length()); + } 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) { try { -- 2.39.5 From ddc0673e9541bc00d6a2bbd7ff0073268fd0643a Mon Sep 17 00:00:00 2001 From: nenzen Date: Thu, 5 Jun 2025 19:14:03 +0200 Subject: [PATCH 2/3] Add type hints --- .../su/apimposter/service/MockService.java | 76 +++++++++++++++++-- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/src/main/java/dsv/su/apimposter/service/MockService.java b/src/main/java/dsv/su/apimposter/service/MockService.java index 08611e5..cec8528 100644 --- a/src/main/java/dsv/su/apimposter/service/MockService.java +++ b/src/main/java/dsv/su/apimposter/service/MockService.java @@ -6,6 +6,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; 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.ConditionalResponse; @@ -28,6 +30,7 @@ public class MockService { private final MockFileLoader loader; 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, @@ -113,7 +116,12 @@ public class MockService { /** * 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 pathVariables Extracted values from the request URL path. * @return Response body with all placeholders replaced. @@ -134,20 +142,74 @@ public class MockService { .toList(); } else if (body instanceof String str) { - for (var entry : pathVariables.entrySet()) { - str = str.replace("{" + entry.getKey() + "}", entry.getValue()); + 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()) { + 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()) { - str = str.replace("{globals." + entry.getKey() + "}", entry.getValue().toString()); + // Otherwise: perform inline replacement for all placeholders in the string + 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))); } - - return str; + matcher.appendTail(sb); + return sb.toString(); } 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 pathVariables, Map 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. * If serialization fails, returns an error wrapper instead. -- 2.39.5 From 82002ce730ad2735fbb53b99f439cb7df40ba64f Mon Sep 17 00:00:00 2001 From: nenzen Date: Thu, 5 Jun 2025 19:14:43 +0200 Subject: [PATCH 3/3] Update and improve README with better examples and new functionality --- README.md | 149 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 100 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 1b68176..d8aa966 100644 --- a/README.md +++ b/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. - **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. - **Conditional Responses:** Return different mock responses based on request data (headers, query, body, or path). - **Swagger UI:** Automatically generated Swagger UI from YAML files. -- **Quick Setup:** Easy installation and configuration. --- ## 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/ ├── globals.yaml ├── system1/ │ ├── users.yaml -│ └── program.yaml +│ └── index.yaml └── another-system/ ├── endpoint1.yaml └── 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 -# 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: currentUserId: "user-123" ``` -```yaml -# 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 //system1/users/123` | `{ "id": "123", "name": "Example User" }` | -| `mocks/system1/program.yaml` | `GET //system1/program` | `{ "programId": 12345, "globalUserId": "user-123" }` | -| `mocks/another-system/subdir/endpoint2` | `GET //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. - +You can reference global values using `{globals.key}` or `{globals.key:type}` in 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. @@ -184,7 +235,7 @@ This is useful for simulating realistic API behavior, such as authentication, fi body: products: - id: "t1" - name: "Mechanical Keyboard" + name: "Keyboard" response: status: 200 headers: { Content-Type: "application/json" } -- 2.39.5