Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions src/main/java/io/naftiko/engine/Resolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*/
package io.naftiko.engine;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.restlet.Request;
Expand Down Expand Up @@ -55,7 +56,8 @@ public static String resolveMustacheTemplate(String template, Map<String, Object
return template;
}

return Mustache.compiler().defaultValue("").compile(template).execute(parameters);
return Mustache.compiler().defaultValue("").compile(template)
.execute(new HashMap<>(parameters));
}

/**
Expand Down Expand Up @@ -142,6 +144,13 @@ public static Object resolveInputParameterFromRequest(InputParameterSpec spec, R

/**
* Apply a list of input parameter specs to a client request (headers and query params).
*
* Resolution priority for parameter values:
* 1. 'value' field - resolved with Mustache template syntax ({{paramName}}) for dynamic resolution
* 2. 'const' field - used as-is, no template resolution applied
* 3. 'template' field - resolved with Mustache syntax
* 4. Parameters map - direct lookup by parameter name
* 5. Environment variables - for 'environment' location
*/
public static void resolveInputParametersToRequest(Request clientRequest,
List<InputParameterSpec> specs, Map<String, Object> parameters) {
Expand All @@ -156,8 +165,10 @@ public static void resolveInputParametersToRequest(Request clientRequest,
Object val = null;

if (spec.getValue() != null) {
val = spec.getValue();
// Resolve Mustache templates in value, allowing dynamic parameter resolution
val = Resolver.resolveMustacheTemplate(spec.getValue(), parameters);
} else if (spec.getConstant() != null) {
// Use constant value as-is (no template resolution)
val = spec.getConstant();
} else if (spec.getTemplate() != null) {
val = Resolver.resolveMustacheTemplate(spec.getTemplate(), parameters);
Expand All @@ -169,7 +180,7 @@ public static void resolveInputParametersToRequest(Request clientRequest,

if (val == null) {
continue;
}else{
} else if (parameters != null) {
parameters.put(spec.getName(), val);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public HttpClientOperationSpec getOperationSpec(String operationName) {
public void setHeaders(Request request) {
// Set any default headers from the input parameters
for (InputParameterSpec param : getHttpClientSpec().getInputParameters()) {
if ("header".equals(param.getIn())) {
if ("header".equalsIgnoreCase(param.getIn()) && param.getConstant() != null) {
request.getHeaders().set(param.getName(), param.getConstant());
}
}
Expand Down Expand Up @@ -120,7 +120,7 @@ public void setChallengeResponse(Request serverRequest, Request clientRequest, S
(BearerAuthenticationSpec) authenticationSpec;
challengeResponse = new ChallengeResponse(ChallengeScheme.HTTP_OAUTH_BEARER);
challengeResponse.setRawValue(
Resolver.resolveMustacheTemplate(bearerAuth.getToken(), parameters));
Resolver.resolveMustacheTemplate(bearerAuth.getToken(), parameters));
clientRequest.setChallengeResponse(challengeResponse);
break;

Expand Down
62 changes: 36 additions & 26 deletions src/main/java/io/naftiko/engine/exposes/ApiResourceRestlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -390,33 +390,43 @@ private boolean handleFromForwardSpec(Request request, Response response) {

if (httpAdapter.getHttpClientSpec().getNamespace()
.equals(forwardSpec.getTargetNamespace())) {
// Prepare the HTTP client request
String path = (String) request.getAttributes().get("path");
String targetRef = httpAdapter.getHttpClientSpec().getBaseUri() + path;
Request clientRequest = new Request(request.getMethod(), targetRef);
clientRequest.setEntity(request.getEntity());

// Copy trusted headers from the original request to the client request
copyTrustedHeaders(request, clientRequest,
getResourceSpec().getForward().getTrustedHeaders());

// Resolve HTTP client input parameters for authentication template resolution
Map<String, Object> parameters = new ConcurrentHashMap<>();
Resolver.resolveInputParametersToRequest(clientRequest,
try {
// Prepare the HTTP client request
String path = (String) request.getAttributes().get("path");
String targetRef = httpAdapter.getHttpClientSpec().getBaseUri() + path;
Request clientRequest = new Request(request.getMethod(), targetRef);
clientRequest.setEntity(request.getEntity());

// Copy trusted headers from the original request to the client request
copyTrustedHeaders(request, clientRequest,
getResourceSpec().getForward().getTrustedHeaders());

// Prepare parameters map for template resolution
Map<String, Object> parameters = new ConcurrentHashMap<>();

// Resolve client input parameters first so authentication templates
// (e.g. bearer token from environment) can be resolved correctly
Resolver.resolveInputParametersToRequest(clientRequest,
httpAdapter.getHttpClientSpec().getInputParameters(), parameters);

// Set any authentication needed on the client request
Response clientResponse = new Response(clientRequest);
httpAdapter.setChallengeResponse(request, clientRequest,
clientRequest.getResourceRef().toString(), parameters);
httpAdapter.setHeaders(clientRequest);

// Send the request to the target endpoint
httpAdapter.getHttpClient().handle(clientRequest, clientResponse);
response.setStatus(clientResponse.getStatus());
response.setEntity(clientResponse.getEntity());
response.commit();
return true;

// Set any authentication needed on the client request
Response clientResponse = new Response(clientRequest);
httpAdapter.setChallengeResponse(request, clientRequest,
clientRequest.getResourceRef().toString(), parameters);
httpAdapter.setHeaders(clientRequest);

// Send the request to the target endpoint
httpAdapter.getHttpClient().handle(clientRequest, clientResponse);
response.setStatus(clientResponse.getStatus());
response.setEntity(clientResponse.getEntity());
response.commit();
return true;
} catch (Exception e) {
response.setStatus(Status.SERVER_ERROR_INTERNAL);
response.setEntity("Error while handling an HTTP client call\n\n" + e.toString(),
MediaType.TEXT_PLAIN);
return true;
}
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/io/naftiko/spec/InputParameterSpec.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ public class InputParameterSpec extends StructureSpec<InputParameterSpec> {
private volatile String template;

/**
* Value of the parameter. Can be a static value or an expression resolved from the execution
* context (e.g. {@code $this.namespace.param}).
* Value of the parameter. Supports Mustache template syntax ({{paramName}}) for dynamic
* resolution from the execution context. Provides an alternative to 'const' that allows
* parameter variable substitution. Takes precedence over 'const'.
*/
private volatile String value;

Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/schemas/tutorial/step3a-encapsulate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
---
naftiko: "0.4"
info:
label: "Tutorial - Step 3a - Forwarding API Resource"
label: "Tutorial - Step 3a - Encapsulating HTTP source"
description: "This is a sample capability specification to demonstrate the features of Naftiko"
tags:
- Naftiko
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/schemas/tutorial/step3b-encapsulate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
---
naftiko: "0.4"
info:
label: "Tutorial - Step 3b - Forwarding API Resource"
label: "Tutorial - Step 3b - Encapsulating HTTP source"
description: "This is a sample capability specification to demonstrate the features of Naftiko"
tags:
- Naftiko
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/schemas/tutorial/step3c-encapsulate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
---
naftiko: "0.4"
info:
label: "Tutorial - Step 3b - Forwarding API Resource"
label: "Tutorial - Step 3b - Encapsulating HTTP source"
description: "This is a sample capability specification to demonstrate the features of Naftiko"
tags:
- Naftiko
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/schemas/tutorial/step4a-structure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
---
naftiko: "0.4"
info:
label: "Tutorial - Step 3b - Forwarding API Resource"
label: "Tutorial - Step 4a - Structured API resource"
description: "This is a sample capability specification to demonstrate the features of Naftiko"
tags:
- Naftiko
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/schemas/tutorial/step4b-structure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
naftiko: "0.4"

info:
label: "Tutorial - Step 3b - Forwarding API Resource"
label: "Tutorial - Step 4b - Structured API resource"
description: "This is a sample capability specification to demonstrate the features of Naftiko"
tags:
- Naftiko
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* Copyright 2025-2026 Naftiko
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.naftiko.engine;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.Test;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import io.naftiko.Capability;
import io.naftiko.engine.exposes.ApiResourceRestlet;
import io.naftiko.engine.exposes.ApiServerAdapter;
import io.naftiko.spec.NaftikoSpec;
import io.naftiko.spec.exposes.ApiServerResourceSpec;
import io.naftiko.spec.exposes.ApiServerSpec;

public class CapabilityForwardHeaderIntegrationTest {

@Test
public void forwardShouldSetNotionVersionHeaderFromValueField() throws Exception {
AtomicReference<String> receivedVersion = new AtomicReference<>();

HttpServer mockServer = HttpServer.create(new InetSocketAddress(0), 0);
int mockPort = mockServer.getAddress().getPort();
mockServer.createContext("/v1/pages", new HttpHandler() {
@Override
public void handle(HttpExchange exchange) {
try {
receivedVersion.set(exchange.getRequestHeaders().getFirst("Notion-Version"));
byte[] body = "{\"ok\":true}".getBytes();
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, body.length);
exchange.getResponseBody().write(body);
} catch (Exception ignored) {
} finally {
try {
exchange.close();
} catch (Exception ignored) {
}
}
}
});
mockServer.start();

try {
String yaml = """
naftiko: \"0.4\"
capability:
exposes:
- type: \"api\"
address: \"localhost\"
port: 0
namespace: \"sample\"
resources:
- path: \"/notion/{{path}}\"
forward:
targetNamespace: notion
trustedHeaders:
- Notion-Version

consumes:
- type: \"http\"
namespace: \"notion\"
baseUri: \"http://localhost:%d/v1/\"
inputParameters:
- name: \"Notion-Version\"
in: \"header\"
value: \"2025-09-03\"
""".formatted(mockPort);

ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
NaftikoSpec spec = mapper.readValue(yaml, NaftikoSpec.class);

Capability capability = new Capability(spec);
ApiServerAdapter adapter = (ApiServerAdapter) capability.getServerAdapters().get(0);
ApiServerSpec serverSpec = (ApiServerSpec) adapter.getSpec();
ApiServerResourceSpec resourceSpec = serverSpec.getResources().get(0);
ApiResourceRestlet restlet = new ApiResourceRestlet(capability, serverSpec, resourceSpec);

Request request = new Request(org.restlet.data.Method.GET,
"http://localhost/notion/pages");
request.getAttributes().put("path", "pages");
Response response = new Response(request);

Method forwardMethod = ApiResourceRestlet.class.getDeclaredMethod("handleFromForwardSpec",
Request.class, Response.class);
forwardMethod.setAccessible(true);
boolean handled = (boolean) forwardMethod.invoke(restlet, request, response);

assertTrue(handled, "Forward request should be handled");
assertEquals(Status.SUCCESS_OK, response.getStatus(), "Forward response should be 200");
assertNotNull(response.getEntity(), "Forward response should include downstream entity");
assertEquals(MediaType.APPLICATION_JSON, response.getEntity().getMediaType(),
"Forward response should be json");

assertEquals("2025-09-03", receivedVersion.get(),
"Notion-Version should be set from inputParameters.value in forward mode");
} finally {
mockServer.stop(0);
}
}
}
Loading