tools: nfdc face create command accepts local FaceUri

refs #4017

Change-Id: I2d13403f13c30146c53744828ba2a37a6fab7de2
diff --git a/docs/manpages/nfdc-face.rst b/docs/manpages/nfdc-face.rst
index f393a93..cfec8e2 100644
--- a/docs/manpages/nfdc-face.rst
+++ b/docs/manpages/nfdc-face.rst
@@ -3,9 +3,9 @@
 
 SYNOPSIS
 --------
-| nfdc face [list]
+| nfdc face [list [[remote] <FACEURI>] [local <FACEURI>] [scheme <SCHEME>]]
 | nfdc face show [id] <FACEID>
-| nfdc face create [remote] <FACEURI> [[persistency] <PERSISTENCY>]
+| nfdc face create [remote] <FACEURI> [[persistency] <PERSISTENCY>] [local <FACEURI>]
 | nfdc face destroy [face] <FACEID|FACEURI>
 | nfdc channel [list]
 
@@ -16,11 +16,14 @@
 an overlay communication channel between NFD and a remote node,
 or an inter-process communication channel between NFD and a local application.
 
-The **nfdc face list** command shows a list of faces, their properties, and statistics.
+The **nfdc face list** command shows a list of faces, their properties, and statistics,
+optionally filtered by remote endpoint, local endpoint, and FaceUri scheme.
+When multiple filters are specified, returned faces must satisfy all filters.
 
 The **nfdc face show** command shows properties and statistics of one specific face.
 
-The **nfdc face create** command creates a unicast UDP or TCP face.
+The **nfdc face create** command creates a UDP unicast, TCP, or Ethernet unicast face.
+Local FaceUri is required for creating Ethernet unicast faces; otherwise it must be omitted.
 
 The **nfdc face destroy** command destroys an existing face.
 
@@ -34,14 +37,30 @@
     It is displayed in the output of **nfdc face list** and **nfdc face create** commands.
 
 <FACEURI>
-    An URI representing the remote endpoint of a face.
-    Its syntax is:
+    A URI representing the remote or local endpoint of a face.
+    Examples:
 
-    - udp[4|6]://<IP-or-host>[:<port>]
-    - tcp[4|6]://<IP-or-host>[:<port>]
+    - udp4://192.0.2.1:6363
+    - udp6://[2001:db8::1]:6363
+    - udp://example.net
+    - tcp4://192.0.2.1:6363
+    - tcp6://[2001:db8::1]:6363
+    - tcp://example.net
+    - unix:///var/run/nfd.sock
+    - fd://6
+    - ether://[08:00:27:01:01:01]
+    - dev://eth0
 
     When a hostname is specified, a DNS query is used to obtain the IP address.
 
+<SCHEME>
+    The scheme portion of either remote or local endpoint.
+    Examples:
+
+    - udp4
+    - unix
+    - dev
+
 <PERSISTENCY>
     Either "persistent" or "permanent".
     A "persistent" face (the default) is closed when a socket error occurs.
diff --git a/tests/tools/nfdc/face-module.t.cpp b/tests/tools/nfdc/face-module.t.cpp
index bce68b7..de68fe6 100644
--- a/tests/tools/nfdc/face-module.t.cpp
+++ b/tests/tools/nfdc/face-module.t.cpp
@@ -247,6 +247,7 @@
     ControlParameters req = MOCK_NFD_MGMT_REQUIRE_COMMAND_IS("/localhost/nfd/faces/create");
     BOOST_REQUIRE(req.hasUri());
     BOOST_CHECK_EQUAL(req.getUri(), "udp4://159.242.33.78:6363");
+    BOOST_CHECK(!req.hasLocalUri());
     BOOST_REQUIRE(req.hasFacePersistency());
     BOOST_CHECK_EQUAL(req.getFacePersistency(), FacePersistency::FACE_PERSISTENCY_PERSISTENT);
 
@@ -262,6 +263,29 @@
   BOOST_CHECK(err.is_empty());
 }
 
+BOOST_AUTO_TEST_CASE(CreatingWithLocalUri)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    ControlParameters req = MOCK_NFD_MGMT_REQUIRE_COMMAND_IS("/localhost/nfd/faces/create");
+    BOOST_REQUIRE(req.hasUri());
+    BOOST_CHECK_EQUAL(req.getUri(), "udp4://22.91.89.51:19903");
+    BOOST_REQUIRE(req.hasLocalUri());
+    BOOST_CHECK_EQUAL(req.getLocalUri(), "udp4://98.68.23.71:6363");
+    BOOST_REQUIRE(req.hasFacePersistency());
+    BOOST_CHECK_EQUAL(req.getFacePersistency(), FacePersistency::FACE_PERSISTENCY_PERMANENT);
+
+    ControlParameters resp;
+    resp.setFaceId(301)
+        .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERMANENT);
+    this->succeedCommand(interest, resp);
+  };
+
+  this->execute("face create udp://22.91.89.51:19903 permanent local udp://98.68.23.71");
+  BOOST_CHECK_EQUAL(exitCode, 0);
+  BOOST_CHECK(out.is_equal("face-created id=301 remote=udp4://22.91.89.51:19903 persistency=permanent\n"));
+  BOOST_CHECK(err.is_empty());
+}
+
 BOOST_AUTO_TEST_CASE(UpgradingPersistency)
 {
   bool hasUpdateCommand = false;
@@ -319,6 +343,22 @@
   BOOST_CHECK(err.is_empty());
 }
 
+BOOST_AUTO_TEST_CASE(ErrorCanonizeRemote)
+{
+  this->execute("face create invalid://");
+  BOOST_CHECK_EQUAL(exitCode, 4);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error when canonizing 'invalid://': scheme not supported\n"));
+}
+
+BOOST_AUTO_TEST_CASE(ErrorCanonizeLocal)
+{
+  this->execute("face create udp4://24.37.20.47:6363 local invalid://");
+  BOOST_CHECK_EQUAL(exitCode, 4);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error when canonizing 'invalid://': scheme not supported\n"));
+}
+
 BOOST_AUTO_TEST_CASE(ErrorCreate)
 {
   this->processInterest = nullptr; // no response
diff --git a/tools/nfdc/face-module.cpp b/tools/nfdc/face-module.cpp
index f0c9dfc..2ec3719 100644
--- a/tools/nfdc/face-module.cpp
+++ b/tools/nfdc/face-module.cpp
@@ -52,7 +52,8 @@
   defFaceCreate
     .setTitle("create a face")
     .addArg("remote", ArgValueType::FACE_URI, Required::YES, Positional::YES)
-    .addArg("persistency", ArgValueType::FACE_PERSISTENCY, Required::NO, Positional::YES);
+    .addArg("persistency", ArgValueType::FACE_PERSISTENCY, Required::NO, Positional::YES)
+    .addArg("local", ArgValueType::FACE_URI, Required::NO, Positional::NO);
   parser.addCommand(defFaceCreate, &FaceModule::create);
 
   CommandDefinition defFaceDestroy("face", "destroy");
@@ -147,23 +148,30 @@
 void
 FaceModule::create(ExecuteContext& ctx)
 {
-  auto faceUri = ctx.args.get<FaceUri>("remote");
+  auto remoteUri = ctx.args.get<FaceUri>("remote");
+  auto localUri = ctx.args.getOptional<FaceUri>("local");
   auto persistency = ctx.args.get<FacePersistency>("persistency", FacePersistency::FACE_PERSISTENCY_PERSISTENT);
-  FaceUri canonicalUri;
+  FaceUri canonicalRemote;
+  ndn::optional<FaceUri> canonicalLocal;
+
+  auto handleCanonizeError = [&] (const FaceUri& faceUri, const std::string& error) {
+    ctx.exitCode = 4;
+    ctx.err << "Error when canonizing '" << faceUri << "': " << error << '\n';
+  };
 
   auto printPositiveResult = [&] (const std::string& actionSummary, const ControlParameters& resp) {
     text::ItemAttributes ia;
     ctx.out << actionSummary << ' '
             << ia("id") << resp.getFaceId()
-            << ia("remote") << canonicalUri
+            << ia("remote") << canonicalRemote
             << ia("persistency") << resp.getFacePersistency()
             << '\n';
-    ///\todo #3956 display local=localUri before 'remote' field
+    ///\todo #3956 display local FaceUri before 'remote' field
   };
 
   auto handle409 = [&] (const ControlResponse& resp) {
     ControlParameters respParams(resp.getBody());
-    if (respParams.getUri() != canonicalUri.toString()) {
+    if (respParams.getUri() != canonicalRemote.toString()) {
       // we are conflicting with a different face, which is a general error
       return false;
     }
@@ -183,24 +191,43 @@
     return true;
   };
 
-  faceUri.canonize(
-    [&] (const FaceUri& canonicalUri1) {
-      canonicalUri = canonicalUri1;
-      ctx.controller.start<ndn::nfd::FaceCreateCommand>(
-        ControlParameters().setUri(canonicalUri.toString()).setFacePersistency(persistency),
-        bind(printPositiveResult, "face-created", _1),
-        [&] (const ControlResponse& resp) {
-          if (resp.getCode() == 409 && handle409(resp)) {
-            return;
-          }
-          ctx.makeCommandFailureHandler("creating face")(resp); // invoke general error handler
-        },
-        ctx.makeCommandOptions());
+  auto doCreateFace = [&] {
+    ControlParameters params;
+    params.setUri(canonicalRemote.toString());
+    if (canonicalLocal) {
+      params.setLocalUri(canonicalLocal->toString());
+    }
+    params.setFacePersistency(persistency);
+
+    ctx.controller.start<ndn::nfd::FaceCreateCommand>(
+      params,
+      bind(printPositiveResult, "face-created", _1),
+      [&] (const ControlResponse& resp) {
+        if (resp.getCode() == 409 && handle409(resp)) {
+          return;
+        }
+        ctx.makeCommandFailureHandler("creating face")(resp); // invoke general error handler
+      },
+      ctx.makeCommandOptions());
+  };
+
+  remoteUri.canonize(
+    [&] (const FaceUri& canonicalUri) {
+      canonicalRemote = canonicalUri;
+      if (localUri) {
+        localUri->canonize(
+          [&] (const FaceUri& canonicalUri) {
+            canonicalLocal = canonicalUri;
+            doCreateFace();
+          },
+          bind(handleCanonizeError, *localUri, _1),
+          ctx.face.getIoService(), ctx.getTimeout());
+      }
+      else {
+        doCreateFace();
+      }
     },
-    [&] (const std::string& canonizeError) {
-      ctx.exitCode = 4;
-      ctx.err << "Error when canonizing FaceUri: " << canonizeError << '\n';
-    },
+    bind(handleCanonizeError, remoteUri, _1),
     ctx.face.getIoService(), ctx.getTimeout());
 
   ctx.face.processEvents();