Eliminate NormalizedNodePayload
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / transactions / RestconfStrategy.java
index 6f277134e233afa30f61dfc24b38ec5ad90ab3e7..074f466284b637f31de40bb5f6c2a74cd9616411 100644 (file)
@@ -13,8 +13,6 @@ import static org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes.fr
 
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSetMultimap;
 import com.google.common.io.CharSource;
 import com.google.common.util.concurrent.FutureCallback;
 import com.google.common.util.concurrent.Futures;
@@ -24,11 +22,8 @@ import java.io.IOException;
 import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Comparator;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Map.Entry;
 import java.util.NoSuchElementException;
 import java.util.Optional;
 import java.util.concurrent.CancellationException;
@@ -55,6 +50,7 @@ import org.opendaylight.mdsal.dom.api.DOMTransactionChain;
 import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
 import org.opendaylight.netconf.dom.api.NetconfDataTreeService;
 import org.opendaylight.restconf.api.ApiPath;
+import org.opendaylight.restconf.api.FormattableBody;
 import org.opendaylight.restconf.api.query.ContentParam;
 import org.opendaylight.restconf.api.query.WithDefaultsParam;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
@@ -62,40 +58,41 @@ import org.opendaylight.restconf.common.errors.RestconfError;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
 import org.opendaylight.restconf.common.patch.PatchContext;
-import org.opendaylight.restconf.common.patch.PatchStatusContext;
-import org.opendaylight.restconf.common.patch.PatchStatusEntity;
 import org.opendaylight.restconf.nb.rfc8040.Insert;
 import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
-import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
-import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
-import org.opendaylight.restconf.nb.rfc8040.utils.parser.YangInstanceIdentifierSerializer;
 import org.opendaylight.restconf.server.api.ChildBody;
 import org.opendaylight.restconf.server.api.ConfigurationMetadata;
+import org.opendaylight.restconf.server.api.CreateResourceResult;
 import org.opendaylight.restconf.server.api.DataGetParams;
 import org.opendaylight.restconf.server.api.DataGetResult;
-import org.opendaylight.restconf.server.api.DataPatchPath;
 import org.opendaylight.restconf.server.api.DataPatchResult;
 import org.opendaylight.restconf.server.api.DataPostBody;
-import org.opendaylight.restconf.server.api.DataPostPath;
 import org.opendaylight.restconf.server.api.DataPostResult;
-import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
-import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
-import org.opendaylight.restconf.server.api.DataPutPath;
 import org.opendaylight.restconf.server.api.DataPutResult;
 import org.opendaylight.restconf.server.api.DataYangPatchResult;
 import org.opendaylight.restconf.server.api.DatabindContext;
+import org.opendaylight.restconf.server.api.DatabindPath;
+import org.opendaylight.restconf.server.api.DatabindPath.Action;
+import org.opendaylight.restconf.server.api.DatabindPath.Data;
+import org.opendaylight.restconf.server.api.DatabindPath.InstanceReference;
+import org.opendaylight.restconf.server.api.DatabindPath.OperationPath;
+import org.opendaylight.restconf.server.api.DatabindPath.Rpc;
+import org.opendaylight.restconf.server.api.InvokeResult;
 import org.opendaylight.restconf.server.api.OperationInputBody;
-import org.opendaylight.restconf.server.api.OperationsGetResult;
-import org.opendaylight.restconf.server.api.OperationsPostPath;
-import org.opendaylight.restconf.server.api.OperationsPostResult;
 import org.opendaylight.restconf.server.api.PatchBody;
+import org.opendaylight.restconf.server.api.PatchStatusContext;
+import org.opendaylight.restconf.server.api.PatchStatusEntity;
 import org.opendaylight.restconf.server.api.ResourceBody;
+import org.opendaylight.restconf.server.api.ServerRequest;
+import org.opendaylight.restconf.server.spi.ApiPathCanonizer;
 import org.opendaylight.restconf.server.spi.ApiPathNormalizer;
-import org.opendaylight.restconf.server.spi.ApiPathNormalizer.InstanceReference;
-import org.opendaylight.restconf.server.spi.ApiPathNormalizer.OperationPath;
-import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.Data;
-import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.Rpc;
+import org.opendaylight.restconf.server.spi.DefaultResourceContext;
+import org.opendaylight.restconf.server.spi.HttpGetResource;
+import org.opendaylight.restconf.server.spi.NormalizedFormattableBody;
+import org.opendaylight.restconf.server.spi.NormalizedNodeWriterFactory;
 import org.opendaylight.restconf.server.spi.OperationInput;
+import org.opendaylight.restconf.server.spi.OperationOutputBody;
+import org.opendaylight.restconf.server.spi.OperationsResource;
 import org.opendaylight.restconf.server.spi.RpcImplementation;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.with.defaults.rev110601.WithDefaultsMode;
 import org.opendaylight.yangtools.yang.common.Empty;
@@ -103,9 +100,7 @@ import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.QNameModule;
-import org.opendaylight.yangtools.yang.common.Revision;
 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
-import org.opendaylight.yangtools.yang.common.XMLNamespace;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
@@ -141,10 +136,7 @@ import org.opendaylight.yangtools.yang.model.api.source.SourceRepresentation;
 import org.opendaylight.yangtools.yang.model.api.source.YangTextSource;
 import org.opendaylight.yangtools.yang.model.api.source.YinTextSource;
 import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
-import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.api.stmt.SubmoduleEffectiveStatement;
-import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
-import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -192,10 +184,12 @@ public abstract class RestconfStrategy {
     private final DOMMountPointService mountPointService;
     private final DOMActionService actionService;
     private final DOMRpcService rpcService;
+    private final HttpGetResource operations;
 
     RestconfStrategy(final DatabindContext databind, final ImmutableMap<QName, RpcImplementation> localRpcs,
             final @Nullable DOMRpcService rpcService, final @Nullable DOMActionService actionService,
-            final YangTextSourceExtension sourceProvider, final @Nullable DOMMountPointService mountPointService) {
+            final @Nullable YangTextSourceExtension sourceProvider,
+            final @Nullable DOMMountPointService mountPointService) {
         this.databind = requireNonNull(databind);
         this.localRpcs = requireNonNull(localRpcs);
         this.rpcService = rpcService;
@@ -203,6 +197,7 @@ public abstract class RestconfStrategy {
         this.sourceProvider = sourceProvider;
         this.mountPointService = mountPointService;
         pathNormalizer = new ApiPathNormalizer(databind);
+        operations = new OperationsResource(pathNormalizer);
     }
 
     public final @NonNull StrategyAndPath resolveStrategyPath(final ApiPath path) {
@@ -261,8 +256,8 @@ public abstract class RestconfStrategy {
         }
         final var dataBroker = mountPoint.getService(DOMDataBroker.class);
         if (dataBroker.isPresent()) {
-            return new MdsalRestconfStrategy(mountDatabind, dataBroker.orElseThrow(), rpcService, actionService,
-                sourceProvider, mountPointService);
+            return new MdsalRestconfStrategy(mountDatabind, dataBroker.orElseThrow(), ImmutableMap.of(), rpcService,
+                actionService, sourceProvider, mountPointService);
         }
         LOG.warn("Mount point {} does not expose a suitable access interface", mountPath);
         throw new RestconfDocumentedException("Could not find a supported access interface in mount point",
@@ -331,8 +326,8 @@ public abstract class RestconfStrategy {
         }, MoreExecutors.directExecutor());
     }
 
-    public @NonNull RestconfFuture<DataPutResult> dataPUT(final ApiPath apiPath, final ResourceBody body,
-            final Map<String, String> queryParameters) {
+    public @NonNull RestconfFuture<DataPutResult> dataPUT(final ServerRequest request, final ApiPath apiPath,
+            final ResourceBody body) {
         final Data path;
         try {
             path = pathNormalizer.normalizeDataPath(apiPath);
@@ -342,14 +337,14 @@ public abstract class RestconfStrategy {
 
         final Insert insert;
         try {
-            insert = Insert.ofQueryParameters(databind, queryParameters);
+            insert = Insert.of(databind, request.queryParameters());
         } catch (IllegalArgumentException e) {
             return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
         }
         final NormalizedNode data;
         try {
-            data = body.toNormalizedNode(new DataPutPath(databind, path.inference(), path.instance()));
+            data = body.toNormalizedNode(path);
         } catch (RestconfDocumentedException e) {
             return RestconfFuture.failed(e);
         }
@@ -500,7 +495,7 @@ public abstract class RestconfStrategy {
      * @param insert  {@link Insert}
      * @return A {@link RestconfFuture}
      */
-    public final @NonNull RestconfFuture<CreateResource> postData(final YangInstanceIdentifier path,
+    public final @NonNull RestconfFuture<CreateResourceResult> postData(final YangInstanceIdentifier path,
             final NormalizedNode data, final @Nullable Insert insert) {
         final ListenableFuture<? extends CommitInfo> future;
         if (insert != null) {
@@ -510,11 +505,11 @@ public abstract class RestconfStrategy {
             future = createAndCommit(prepareWriteExecution(), path, data);
         }
 
-        final var ret = new SettableRestconfFuture<CreateResource>();
+        final var ret = new SettableRestconfFuture<CreateResourceResult>();
         Futures.addCallback(future, new FutureCallback<CommitInfo>() {
             @Override
             public void onSuccess(final CommitInfo result) {
-                ret.set(new CreateResource(new YangInstanceIdentifierSerializer(databind).serializePath(
+                ret.set(new CreateResourceResult(new ApiPathCanonizer(databind).dataToApiPath(
                     data instanceof MapNode mapData && !mapData.isEmpty()
                         ? path.node(mapData.body().iterator().next().name()) : path)));
             }
@@ -588,7 +583,7 @@ public abstract class RestconfStrategy {
 
         final NormalizedNode data;
         try {
-            data = body.toNormalizedNode(new DataPutPath(databind, path.inference(), path.instance()));
+            data = body.toNormalizedNode(path);
         } catch (RestconfDocumentedException e) {
             return RestconfFuture.failed(e);
         }
@@ -606,7 +601,7 @@ public abstract class RestconfStrategy {
 
         final PatchContext patch;
         try {
-            patch = body.toPatchContext(new DataPatchPath(databind, path.instance()));
+            patch = body.toPatchContext(new DefaultResourceContext(path));
         } catch (IOException e) {
             LOG.debug("Error parsing YANG Patch input", e);
             return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
@@ -621,6 +616,7 @@ public abstract class RestconfStrategy {
      * @param patch Patch context to be processed
      * @return {@link PatchStatusContext}
      */
+    @VisibleForTesting
     public final @NonNull RestconfFuture<DataYangPatchResult> patchData(final PatchContext patch) {
         final var editCollection = new ArrayList<PatchStatusEntity>();
         final var tx = prepareWriteExecution();
@@ -695,7 +691,7 @@ public abstract class RestconfStrategy {
         if (!noError) {
             tx.cancel();
             ret.set(new DataYangPatchResult(
-                new PatchStatusContext(modelContext(), patch.patchId(), List.copyOf(editCollection), false, null)));
+                new PatchStatusContext(databind(), patch.patchId(), List.copyOf(editCollection), false, null)));
             return ret;
         }
 
@@ -703,14 +699,14 @@ public abstract class RestconfStrategy {
             @Override
             public void onSuccess(final CommitInfo result) {
                 ret.set(new DataYangPatchResult(
-                    new PatchStatusContext(modelContext(), patch.patchId(), List.copyOf(editCollection), true, null)));
+                    new PatchStatusContext(databind(), patch.patchId(), List.copyOf(editCollection), true, null)));
             }
 
             @Override
             public void onFailure(final Throwable cause) {
                 // if errors occurred during transaction commit then patch failed and global errors are reported
                 ret.set(new DataYangPatchResult(
-                    new PatchStatusContext(modelContext(), patch.patchId(), List.copyOf(editCollection), false,
+                    new PatchStatusContext(databind(), patch.patchId(), List.copyOf(editCollection), false,
                         TransactionUtil.decodeException(cause, "PATCH", null, modelContext()).getErrors())));
             }
         }, MoreExecutors.directExecutor());
@@ -718,7 +714,7 @@ public abstract class RestconfStrategy {
         return ret;
     }
 
-    private void insertWithPointPost(final RestconfTransaction tx, final YangInstanceIdentifier path,
+    private static void insertWithPointPost(final RestconfTransaction tx, final YangInstanceIdentifier path,
             final NormalizedNode data, final PathArgument pointArg, final NormalizedNodeContainer<?> readList,
             final boolean before) {
         tx.remove(path);
@@ -805,8 +801,9 @@ public abstract class RestconfStrategy {
      * @return A {@link RestconfFuture}
      * @throws NullPointerException if {@code apiPath} is {@code null}
      */
+    @NonNullByDefault
     @SuppressWarnings("checkstyle:abbreviationAsWordInName")
-    public final @NonNull RestconfFuture<Empty> dataDELETE(final ApiPath apiPath) {
+    public final RestconfFuture<Empty> dataDELETE(final ServerRequest request, final ApiPath apiPath) {
         final Data path;
         try {
             path = pathNormalizer.normalizeDataPath(apiPath);
@@ -816,37 +813,47 @@ public abstract class RestconfStrategy {
 
         // FIXME: reject empty YangInstanceIdentifier, as datastores may not be deleted
         final var ret = new SettableRestconfFuture<Empty>();
-        delete(ret, path.instance());
+        delete(ret, request, path.instance());
         return ret;
     }
 
-    abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
+    @NonNullByDefault
+    abstract void delete(SettableRestconfFuture<Empty> future, ServerRequest request, YangInstanceIdentifier path);
 
-    public final @NonNull RestconfFuture<DataGetResult> dataGET(final ApiPath apiPath,
-            final DataGetParams params) {
+    public final @NonNull RestconfFuture<DataGetResult> dataGET(final ServerRequest request, final ApiPath apiPath) {
         final Data path;
         try {
             path = pathNormalizer.normalizeDataPath(apiPath);
         } catch (RestconfDocumentedException e) {
             return RestconfFuture.failed(e);
         }
-        return dataGET(path, params);
+
+        final DataGetParams getParams;
+        try {
+            getParams = DataGetParams.of(request.queryParameters());
+        } catch (IllegalArgumentException e) {
+            return RestconfFuture.failed(new RestconfDocumentedException(e,
+                new RestconfError(ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, "Invalid GET /data parameters", null,
+                    e.getMessage())));
+        }
+        return dataGET(request, path, getParams);
     }
 
-    abstract @NonNull RestconfFuture<DataGetResult> dataGET(Data path, DataGetParams params);
+    abstract @NonNull RestconfFuture<DataGetResult> dataGET(ServerRequest request, Data path, DataGetParams params);
 
-    static final @NonNull RestconfFuture<DataGetResult> completeDataGET(final Inference inference,
-            final QueryParameters queryParams, final @Nullable NormalizedNode node,
-            final @Nullable ConfigurationMetadata metadata) {
+    @NonNullByDefault
+    static final RestconfFuture<DataGetResult> completeDataGET(final @Nullable NormalizedNode node, final Data path,
+            final NormalizedNodeWriterFactory writerFactory, final @Nullable ConfigurationMetadata metadata) {
+        // Non-existing data
         if (node == null) {
             return RestconfFuture.failed(new RestconfDocumentedException(
                 "Request could not be completed because the relevant data model content does not exist",
                 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING));
         }
 
-        final var payload = new NormalizedNodePayload(inference, node, queryParams);
-        return RestconfFuture.of(metadata == null ? new DataGetResult(payload)
-            : new DataGetResult(payload, metadata.entityTag(), metadata.lastModified()));
+        final var body = NormalizedFormattableBody.of(path, node, writerFactory);
+        return RestconfFuture.of(metadata == null ? new DataGetResult(body)
+            : new DataGetResult(body, metadata.entityTag(), metadata.lastModified()));
     }
 
     /**
@@ -1239,67 +1246,28 @@ public abstract class RestconfStrategy {
             y -> builder.addChild((T) prepareData(y.getValue(), stateMap.get(y.getKey()))));
     }
 
-    public @NonNull RestconfFuture<OperationsGetResult> operationsGET() {
-        final var modelContext = modelContext();
-        final var modules = modelContext.getModuleStatements();
-        if (modules.isEmpty()) {
-            // No modules, or defensive return empty content
-            return RestconfFuture.of(new OperationsGetResult.Container(modelContext, ImmutableSetMultimap.of()));
-        }
-
-        // RPC QNames by their XMLNamespace/Revision. This should be a Table, but Revision can be null, which wrecks us.
-        final var table = new HashMap<XMLNamespace, Map<Revision, ImmutableSet<QName>>>();
-        for (var entry : modules.entrySet()) {
-            final var module = entry.getValue();
-            final var rpcNames = module.streamEffectiveSubstatements(RpcEffectiveStatement.class)
-                .map(RpcEffectiveStatement::argument)
-                .collect(ImmutableSet.toImmutableSet());
-            if (!rpcNames.isEmpty()) {
-                final var namespace = entry.getKey();
-                table.computeIfAbsent(namespace.namespace(), ignored -> new HashMap<>())
-                    .put(namespace.revision(), rpcNames);
-            }
-        }
-
-        // Now pick the latest revision for each namespace
-        final var rpcs = ImmutableSetMultimap.<QNameModule, QName>builder();
-        for (var entry : table.entrySet()) {
-            entry.getValue().entrySet().stream()
-            .sorted(Comparator.comparing(Entry::getKey, (first, second) -> Revision.compare(second, first)))
-            .findFirst()
-            .ifPresent(row -> rpcs.putAll(QNameModule.of(entry.getKey(), row.getKey()), row.getValue()));
-        }
-        return RestconfFuture.of(new OperationsGetResult.Container(modelContext, rpcs.build()));
+    @NonNullByDefault
+    public RestconfFuture<FormattableBody> operationsGET(final ServerRequest request) {
+        return operations.httpGET(request);
     }
 
-    public @NonNull RestconfFuture<OperationsGetResult> operationsGET(final ApiPath apiPath) {
-        if (apiPath.steps().isEmpty()) {
-            return operationsGET();
-        }
-
-        final Rpc rpc;
-        try {
-            rpc = pathNormalizer.normalizeRpcPath(apiPath);
-        } catch (RestconfDocumentedException e) {
-            return RestconfFuture.failed(e);
-        }
-
-        return RestconfFuture.of(new OperationsGetResult.Leaf(rpc.inference().modelContext(), rpc.rpc().argument()));
+    @NonNullByDefault
+    public RestconfFuture<FormattableBody> operationsGET(final ServerRequest request, final ApiPath apiPath) {
+        return operations.httpGET(request, apiPath);
     }
 
-    public @NonNull RestconfFuture<OperationsPostResult> operationsPOST(final URI restconfURI, final ApiPath apiPath,
-            final OperationInputBody body) {
-        final OperationPath.Rpc path;
+    public @NonNull RestconfFuture<InvokeResult> operationsPOST(final ServerRequest request, final URI restconfURI,
+            final ApiPath apiPath, final OperationInputBody body) {
+        final Rpc path;
         try {
             path = pathNormalizer.normalizeRpcPath(apiPath);
         } catch (RestconfDocumentedException e) {
             return RestconfFuture.failed(e);
         }
 
-        final var postPath = new OperationsPostPath(databind, path.inference());
         final ContainerNode data;
         try {
-            data = body.toContainerNode(postPath);
+            data = body.toContainerNode(path);
         } catch (IOException e) {
             LOG.debug("Error reading input", e);
             return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
@@ -1309,7 +1277,8 @@ public abstract class RestconfStrategy {
         final var type = path.rpc().argument();
         final var local = localRpcs.get(type);
         if (local != null) {
-            return local.invoke(restconfURI, new OperationInput(databind, postPath.operation(), data));
+            return local.invoke(restconfURI, new OperationInput(path, data))
+                .transform(output -> outputToInvokeResult(path, output));
         }
         if (rpcService == null) {
             LOG.debug("RPC invocation is not available");
@@ -1317,13 +1286,13 @@ public abstract class RestconfStrategy {
                 ErrorType.PROTOCOL, ErrorTag.OPERATION_NOT_SUPPORTED));
         }
 
-        final var ret = new SettableRestconfFuture<OperationsPostResult>();
+        final var ret = new SettableRestconfFuture<InvokeResult>();
         Futures.addCallback(rpcService.invokeRpc(type, data), new FutureCallback<DOMRpcResult>() {
             @Override
             public void onSuccess(final DOMRpcResult response) {
                 final var errors = response.errors();
                 if (errors.isEmpty()) {
-                    ret.set(new OperationsPostResult(databind, postPath.operation(), response.value()));
+                    ret.set(outputToInvokeResult(path, response.value()));
                 } else {
                     LOG.debug("RPC invocation reported {}", response.errors());
                     ret.setFailure(new RestconfDocumentedException("RPC implementation reported errors", null,
@@ -1346,6 +1315,12 @@ public abstract class RestconfStrategy {
         return ret;
     }
 
+    private static @NonNull InvokeResult outputToInvokeResult(final @NonNull OperationPath path,
+            final @Nullable ContainerNode value) {
+        return value == null || value.isEmpty() ? InvokeResult.EMPTY
+            : new InvokeResult(new OperationOutputBody(path, value));
+    }
+
     public @NonNull RestconfFuture<CharSource> resolveSource(final SourceIdentifier source,
             final Class<? extends SourceRepresentation> representation) {
         final var src = requireNonNull(source);
@@ -1404,10 +1379,10 @@ public abstract class RestconfStrategy {
             ErrorType.APPLICATION, ErrorTag.DATA_MISSING));
     }
 
-    public final @NonNull RestconfFuture<? extends DataPostResult> dataPOST(final ApiPath apiPath,
-            final DataPostBody body, final Map<String, String> queryParameters) {
-        if (apiPath.steps().isEmpty()) {
-            return dataCreatePOST(body.toResource(), queryParameters);
+    public final @NonNull RestconfFuture<? extends DataPostResult> dataPOST(final ServerRequest request,
+            final ApiPath apiPath, final DataPostBody body) {
+        if (apiPath.isEmpty()) {
+            return dataCreatePOST(request, body.toResource());
         }
         final InstanceReference path;
         try {
@@ -1417,11 +1392,10 @@ public abstract class RestconfStrategy {
         }
         if (path instanceof Data dataPath) {
             try (var resourceBody = body.toResource()) {
-                return dataCreatePOST(new DataPostPath(databind, dataPath.inference(), dataPath.instance()),
-                    resourceBody, queryParameters);
+                return dataCreatePOST(request, dataPath, resourceBody);
             }
         }
-        if (path instanceof OperationPath.Action actionPath) {
+        if (path instanceof Action actionPath) {
             try (var inputBody = body.toOperationInput()) {
                 return dataInvokePOST(actionPath, inputBody);
             }
@@ -1431,18 +1405,16 @@ public abstract class RestconfStrategy {
         return RestconfFuture.failed(new RestconfDocumentedException("Unhandled path " + path));
     }
 
-    public @NonNull RestconfFuture<CreateResource> dataCreatePOST(final ChildBody body,
-            final Map<String, String> queryParameters) {
-        return dataCreatePOST(new DataPostPath(databind,
-            SchemaInferenceStack.of(databind.modelContext()).toInference(), YangInstanceIdentifier.of()), body,
-            queryParameters);
+    public @NonNull RestconfFuture<CreateResourceResult> dataCreatePOST(final ServerRequest request,
+            final ChildBody body) {
+        return dataCreatePOST(request, new DatabindPath.Data(databind), body);
     }
 
-    private @NonNull RestconfFuture<CreateResource> dataCreatePOST(final DataPostPath path, final ChildBody body,
-            final Map<String, String> queryParameters) {
+    private @NonNull RestconfFuture<CreateResourceResult> dataCreatePOST(final ServerRequest request,
+            final DatabindPath.Data path, final ChildBody body) {
         final Insert insert;
         try {
-            insert = Insert.ofQueryParameters(path.databind(), queryParameters);
+            insert = Insert.of(path.databind(), request.queryParameters());
         } catch (IllegalArgumentException e) {
             return RestconfFuture.failed(new RestconfDocumentedException(e.getMessage(),
                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e));
@@ -1460,12 +1432,11 @@ public abstract class RestconfStrategy {
         return ret;
     }
 
-    private @NonNull RestconfFuture<InvokeOperation> dataInvokePOST(final OperationPath.Action path,
-            final OperationInputBody body) {
-        final var inference = path.inference();
+    private @NonNull RestconfFuture<InvokeResult> dataInvokePOST(final @NonNull Action path,
+            final @NonNull OperationInputBody body) {
         final ContainerNode input;
         try {
-            input = body.toContainerNode(new OperationsPostPath(databind, inference));
+            input = body.toContainerNode(path);
         } catch (IOException e) {
             LOG.debug("Error reading input", e);
             return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
@@ -1476,11 +1447,8 @@ public abstract class RestconfStrategy {
             return RestconfFuture.failed(new RestconfDocumentedException("DOMActionService is missing."));
         }
 
-        final var future = dataInvokePOST(actionService, path, input);
-        return future.transform(result -> result.getOutput()
-            .flatMap(output -> output.isEmpty() ? Optional.empty()
-                : Optional.of(new InvokeOperation(new NormalizedNodePayload(inference, output))))
-            .orElse(InvokeOperation.EMPTY));
+        return dataInvokePOST(actionService, path, input)
+            .transform(result -> outputToInvokeResult(path, result.getOutput().orElse(null)));
     }
 
     /**
@@ -1493,7 +1461,7 @@ public abstract class RestconfStrategy {
      * @return {@link DOMActionResult}
      */
     private static RestconfFuture<DOMActionResult> dataInvokePOST(final DOMActionService actionService,
-            final OperationPath.Action path, final @NonNull ContainerNode input) {
+            final Action path, final @NonNull ContainerNode input) {
         final var ret = new SettableRestconfFuture<DOMActionResult>();
 
         Futures.addCallback(actionService.invokeAction(