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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.RelationshipCollectionRoute;
import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.RelationshipInstanceRoute;
import uk.co.compendiumdev.thingifier.adapter.http.apihandlers.route.ThingRoute;
import uk.co.compendiumdev.thingifier.adapter.http.lifecycle.ThingifierApiLifecycleContext;
import uk.co.compendiumdev.thingifier.api.docgen.RoutingVerb;
import uk.co.compendiumdev.thingifier.api.http.ThingifierRequestContext;
import uk.co.compendiumdev.thingifier.api.http.bodyparser.ApiBodyFields;
Expand Down Expand Up @@ -64,6 +65,45 @@ public ApiResponse rejectIfInvalid(
final String rawBody,
final QueryFilterParams queryParams,
final String operationType) {
return rejectIfInvalid(
verb,
publicPath,
route,
context,
bodyFields,
rawBody,
queryParams,
operationType,
null);
}

/**
* Rejects a request when one of the route's operation validators rejects it.
*
* <p>The lifecycle context is supplied for HTTP requests so mounted public paths and internal
* route paths can both be exposed to validators.
*
* @param verb routing verb being processed
* @param publicPath public API path requested by the caller
* @param route resolved Thingifier route target
* @param context active request context after auth and data-scope selection
* @param bodyFields parsed request body fields
* @param rawBody raw request body text
* @param queryParams parsed query parameters
* @param operationType resolved operation type label
* @param lifecycle lifecycle context when processing an HTTP request, otherwise null
* @return rejection response, or null when validation accepts the operation
*/
public ApiResponse rejectIfInvalid(
final RoutingVerb verb,
final String publicPath,
final ThingRoute route,
final ThingifierRequestContext context,
final ApiBodyFields bodyFields,
final String rawBody,
final QueryFilterParams queryParams,
final String operationType,
final ThingifierApiLifecycleContext lifecycle) {
Optional<ThingifierApiRouteRule> routeRule = routeRuleFor(verb, route, publicPath);
if (routeRule.isEmpty() || !routeRule.get().hasApiOperationValidators()) {
return null;
Expand All @@ -78,7 +118,8 @@ public ApiResponse rejectIfInvalid(
bodyFields,
rawBody,
queryParams,
operationType);
operationType,
lifecycle);

for (ApiOperationValidatorDefinition definition :
routeRule.get().apiOperationValidators()) {
Expand All @@ -105,7 +146,8 @@ private ApiOperationValidationContext validationContextFor(
final ApiBodyFields bodyFields,
final String rawBody,
final QueryFilterParams queryParams,
final String operationType) {
final String operationType,
final ThingifierApiLifecycleContext lifecycle) {
EntityDefinition targetEntity = targetEntityFor(route);
String targetEntityName = targetEntity == null ? null : targetEntity.getName();
String targetIdentifier = targetIdentifierFor(route);
Expand All @@ -114,7 +156,11 @@ private ApiOperationValidationContext validationContextFor(

return new ApiOperationValidationContext(
verb,
publicPath,
lifecycle == null ? publicPath : lifecycle.requestPath(),
lifecycle == null ? publicPath : lifecycle.mountedPath(),
lifecycle == null ? publicPath : lifecycle.internalPath(),
lifecycle == null ? null : lifecycle.mountName(),
lifecycle == null ? "" : lifecycle.mountPrefix(),
route,
targetEntityName,
targetIdentifier,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ ThingifierApiOperationContext contextFor(
: (lifecycle == null ? null : lifecycle.requestContext());
return new ThingifierApiOperationContext(
verb,
publicPath,
publicPathFor(publicPath, lifecycle),
mountedPathFor(publicPath, lifecycle),
internalPathFor(publicPath, lifecycle),
lifecycle == null ? null : lifecycle.mountName(),
lifecycle == null ? "" : lifecycle.mountPrefix(),
route,
routeRule,
targetEntityName(route, lifecycle),
Expand All @@ -112,6 +116,21 @@ ThingifierApiOperationContext contextFor(
runtime.apiConfig());
}

private String publicPathFor(
final String publicPath, final ThingifierApiLifecycleContext lifecycle) {
return lifecycle == null ? publicPath : lifecycle.requestPath();
}

private String mountedPathFor(
final String publicPath, final ThingifierApiLifecycleContext lifecycle) {
return lifecycle == null ? publicPath : lifecycle.mountedPath();
}

private String internalPathFor(
final String publicPath, final ThingifierApiLifecycleContext lifecycle) {
return lifecycle == null ? publicPath : lifecycle.internalPath();
}

/**
* Resolves the operation type label for route operation callbacks.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,31 @@ public String path() {
return request.getPath();
}

@Override
public String requestPath() {
return request.getRequestPath();
}

@Override
public String mountedPath() {
return request.getMountedPath();
}

@Override
public String internalPath() {
return request.getPath();
}

@Override
public String mountName() {
return request.getMountName();
}

@Override
public String mountPrefix() {
return request.getMountPrefix();
}

/**
* Returns the API path prefix used when matching scoped hooks and API spec rules.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,41 @@ public interface ThingifierApiLifecycleContextView {
*/
String path();

/**
* Returns the public request path received by Thingifier before mount or prefix stripping.
*
* @return public request path without a leading slash
*/
String requestPath();

/**
* Returns the public mounted path for this request.
*
* @return mounted path without a leading slash
*/
String mountedPath();

/**
* Returns the canonical Thingifier path used by generated handlers.
*
* @return internal route path without a leading slash
*/
String internalPath();

/**
* Returns the active named mount.
*
* @return mount name, or null when no named mount matched
*/
String mountName();

/**
* Returns the active public mount prefix.
*
* @return mount prefix with a leading slash, or empty when no prefix applies
*/
String mountPrefix();

/**
* Returns the mapped Thingifier route.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public final class ThingifierApiOperationContext {

private final RoutingVerb verb;
private final String publicPath;
private final String mountedPath;
private final String internalPath;
private final String mountName;
private final String mountPrefix;
private final ThingRoute route;
private final ThingifierApiRouteRule routeRule;
private final String targetEntityName;
Expand Down Expand Up @@ -81,8 +85,90 @@ public ThingifierApiOperationContext(
final ApiBodyFields parsedRequestBody,
final String rawRequestBody,
final ThingifierApiConfig apiConfig) {
this(
verb,
publicPath,
publicPath,
publicPath,
null,
"",
route,
routeRule,
targetEntityName,
targetIdentifier,
parentEntityName,
parentIdentifier,
relationshipName,
childIdentifier,
dataScopeName,
store,
authenticatedPrincipals,
requestHeaders,
queryParams,
parsedRequestBody,
rawRequestBody,
apiConfig);
}

/**
* Creates a callback context with explicit mounted route details.
*
* <p>Mounted requests expose both the public path requested by the caller and the canonical
* internal path processed by Thingifier. Keeping both values here lets application callbacks
* make route-aware decisions without guessing which prefix was stripped.
*
* @param verb route verb being processed
* @param publicPath public request path
* @param mountedPath active mounted path
* @param internalPath canonical Thingifier route path
* @param mountName active mount name, or null
* @param mountPrefix active mount prefix, or empty
* @param route resolved generated route
* @param routeRule matched route rule that owns the callback
* @param targetEntityName target entity name, or null
* @param targetIdentifier target identifier, or null
* @param parentEntityName relationship parent entity name, or null
* @param parentIdentifier relationship parent identifier, or null
* @param relationshipName relationship route name, or null
* @param childIdentifier relationship child identifier, or null
* @param dataScopeName active data-scope name
* @param store active store
* @param authenticatedPrincipals authenticated principals by scheme name
* @param requestHeaders request headers
* @param queryParams parsed query parameters
* @param parsedRequestBody parsed body fields
* @param rawRequestBody raw request body text
* @param apiConfig active API configuration
*/
public ThingifierApiOperationContext(
final RoutingVerb verb,
final String publicPath,
final String mountedPath,
final String internalPath,
final String mountName,
final String mountPrefix,
final ThingRoute route,
final ThingifierApiRouteRule routeRule,
final String targetEntityName,
final String targetIdentifier,
final String parentEntityName,
final String parentIdentifier,
final String relationshipName,
final String childIdentifier,
final String dataScopeName,
final ThingStore store,
final Map<String, Object> authenticatedPrincipals,
final HttpHeadersBlock requestHeaders,
final QueryFilterParams queryParams,
final ApiBodyFields parsedRequestBody,
final String rawRequestBody,
final ThingifierApiConfig apiConfig) {
this.verb = verb;
this.publicPath = normalizedPublicPath(publicPath);
this.mountedPath = normalizedPublicPath(mountedPath);
this.internalPath = normalizedPublicPath(internalPath);
this.mountName = mountName;
this.mountPrefix = normalizedMountPrefix(mountPrefix);
this.route = route;
this.routeRule = routeRule;
this.targetEntityName = targetEntityName;
Expand Down Expand Up @@ -120,6 +206,42 @@ public String publicPath() {
return publicPath;
}

/**
* Returns the active mounted path requested by the caller.
*
* @return mounted path with a leading slash
*/
public String mountedPath() {
return mountedPath;
}

/**
* Returns the canonical Thingifier path processed by generated handlers.
*
* @return internal route path with a leading slash
*/
public String internalPath() {
return internalPath;
}

/**
* Returns the active public mount name.
*
* @return mount name when a named mount matched, otherwise empty
*/
public Optional<String> mountName() {
return Optional.ofNullable(mountName);
}

/**
* Returns the active public mount prefix.
*
* @return mount prefix with a leading slash, or empty when no prefix applies
*/
public String mountPrefix() {
return mountPrefix;
}

/**
* @return resolved generated route target
*/
Expand Down Expand Up @@ -283,4 +405,11 @@ private String normalizedPublicPath(final String path) {
}
return path.startsWith("/") ? path : "/" + path;
}

private String normalizedMountPrefix(final String prefix) {
if (prefix == null || prefix.isEmpty() || "/".equals(prefix)) {
return prefix == null ? "" : prefix;
}
return prefix.startsWith("/") ? prefix : "/" + prefix;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ public RoutingDefinition addRouting(
return defn;
}

/**
* Adds an already-built route definition.
*
* <p>This is used when documentation is projected through public API mounts. The canonical
* generated route is copied first so mounted routes keep the same security, payload, view,
* response-shape, fixed-resource, and header metadata.
*
* @param routingDefinition route definition to add
*/
public void addRouting(final RoutingDefinition routingDefinition) {
if (routingDefinition != null) {
routings.add(routingDefinition);
}
}

/**
* Registers the model entity under the schema names generated routes may reference.
*
Expand Down
Loading
Loading