michael-hoke commented on code in PR #253:
URL: https://github.com/apache/daffodil-vscode/pull/253#discussion_r963974390


##########
server/core/src/main/scala/org.apache.daffodil.debugger.dap/Parse.scala:
##########
@@ -176,71 +180,333 @@ object Parse {
         case class File(path: Path) extends InfosetOutput
       }
 
+      sealed trait TDMLConfig extends LaunchArgs
+      object TDMLConfig {
+        case class Generate(
+            schemaPath: Path,
+            dataPath: Path,
+            stopOnEntry: Boolean,
+            infosetOutput: LaunchArgs.InfosetOutput,
+            name: String,
+            description: String,
+            path: String
+        ) extends TDMLConfig
+
+        case class Append(
+            schemaPath: Path,
+            dataPath: Path,
+            stopOnEntry: Boolean,
+            infosetOutput: LaunchArgs.InfosetOutput,
+            name: String,
+            description: String,
+            path: String
+        ) extends TDMLConfig
+
+        case class Execute(
+            stopOnEntry: Boolean,
+            infosetOutput: LaunchArgs.InfosetOutput,
+            name: String,
+            description: String,
+            path: String
+        ) extends TDMLConfig
+      }
+
       def parse(arguments: JsonObject): EitherNel[String, LaunchArgs] =
+        // Determine, based on the presence of the tdmlConfig object in the 
launch config, whether
+        //   this is a "normal" DFDL operation or if we should attempt to 
parse the values from
+        //   the tdmlConfig object.
+        Option(arguments.getAsJsonObject("tdmlConfig")) match {
+          case None             => parseManual(arguments)
+          case Some(tdmlConfig) => parseTDML(arguments, tdmlConfig)
+        }
+
+      // Parse a launch config that has been found to not have a tdmlConfig 
object
+      def parseManual(arguments: JsonObject): EitherNel[String, LaunchArgs] =
         (
-          Option(arguments.getAsJsonPrimitive("program"))
-            .toRight("missing 'program' field from launch request")
-            .flatMap(path =>
-              Either
-                .catchNonFatal(Paths.get(path.getAsString))
-                .leftMap(t => s"'program' field from launch request is not a 
valid path: $t")
-                .ensureOr(path => s"program file at $path doesn't 
exist")(_.toFile().exists())
-            )
-            .toEitherNel,
-          Option(arguments.getAsJsonPrimitive("data"))
-            .toRight("missing 'data' field from launch request")
-            .flatMap(path =>
-              Either
-                .catchNonFatal(Paths.get(path.getAsString))
-                .leftMap(t => s"'data' field from launch request is not a 
valid path: $t")
-                .ensureOr(path => s"data file at $path doesn't 
exist")(_.toFile().exists())
-            )
-            .toEitherNel,
-          Option(arguments.getAsJsonPrimitive("stopOnEntry"))
-            .map(_.getAsBoolean())
-            .getOrElse(true)
-            .asRight[String]
-            .toEitherNel,
-          Option(arguments.getAsJsonObject("infosetOutput")) match {
+          parseProgram(arguments),
+          parseData(arguments),
+          parseStopOnEntry(arguments),
+          parseInfosetOutput(arguments)
+        ).parMapN(LaunchArgs.Manual.apply)
+    }
+
+    // Parse a tdmlConfig object from the launch config
+    //
+    // tdmlConfig: {
+    //   action: '',
+    //   name: '',
+    //   description: '',
+    //   path: ''
+    // }
+    //
+    // The action field is parsed first.
+    // If it is a valid action ('generate' | 'append' | 'execute'), create a 
LaunchArgs object of the appropriate type
+    // If it is 'none' or missing, create a LaunchArgs.Manual object. This 
will ignore any other fields in the tdmlConfig object.
+    //
+    // arguments:  Launch config
+    // tdmlConfig: tdmlConfig object from the launch config
+    def parseTDML(arguments: JsonObject, tdmlConfig: JsonObject): 
EitherNel[String, LaunchArgs] =
+      Option(tdmlConfig.getAsJsonPrimitive("action")) match {
+        case None =>
+          (
+            parseProgram(arguments),
+            parseData(arguments),
+            parseStopOnEntry(arguments),
+            parseInfosetOutput(arguments)
+          ).parMapN(LaunchArgs.Manual.apply)
+        case Some(action) =>
+          action.getAsString() match {
+            case "generate" =>
+              (
+                parseProgram(arguments),
+                parseData(arguments),
+                parseStopOnEntry(arguments),
+                parseInfosetOutput(arguments, true),
+                parseTDMLName(tdmlConfig),
+                parseTDMLDescription(tdmlConfig),
+                parseTDMLPath(tdmlConfig)
+              ).parMapN(LaunchArgs.TDMLConfig.Generate.apply)
+            case "append" =>
+              (
+                parseProgram(arguments),
+                parseData(arguments),
+                parseStopOnEntry(arguments),
+                parseInfosetOutput(arguments, true),
+                parseTDMLName(tdmlConfig),
+                parseTDMLDescription(tdmlConfig),
+                parseTDMLPath(tdmlConfig)
+              ).parMapN(LaunchArgs.TDMLConfig.Append.apply)
+            case "execute" =>
+              (
+                parseStopOnEntry(arguments),
+                parseInfosetOutput(arguments),
+                parseTDMLName(tdmlConfig),
+                parseTDMLDescription(tdmlConfig),
+                parseTDMLPath(tdmlConfig)
+              ).parMapN(LaunchArgs.TDMLConfig.Execute.apply)
+            case "none" =>
+              (
+                parseProgram(arguments),
+                parseData(arguments),
+                parseStopOnEntry(arguments),
+                parseInfosetOutput(arguments)
+              ).parMapN(LaunchArgs.Manual.apply)
+            case invalidType =>
+              Left(
+                s"invalid 'tdmlConfig.action': '$invalidType', must be 'none', 
'generate', 'append', or 'execute'"
+              ).toEitherNel
+          }
+      }
+
+    // Parse the program field from the launch config
+    // Returns an error if the program field is missing or is an invalid path
+    // A case where the user expects a new directory to be created is not a 
valid path
+    //   eg. /path/to/<existing>/<non-existing>/file.tdml
+    //
+    // arguments: Launch config
+    def parseProgram(arguments: JsonObject) =
+      Option(arguments.getAsJsonPrimitive("program"))
+        .toRight("missing 'program' field from launch request")
+        .flatMap(path =>
+          Either
+            .catchNonFatal(Paths.get(path.getAsString))
+            .leftMap(t => s"'program' field from launch request is not a valid 
path: $t")
+            .ensureOr(path => s"program file at $path doesn't 
exist")(_.toFile().exists())
+        )
+        .toEitherNel
+
+    // Parse the data field from the launch config
+    // Returns an error if the data field is missing or is an invalid path
+    // A case where the user expects a new directory to be created is not a 
valid path
+    //   eg. /path/to/<existing>/<non-existing>/file.tdml
+    //
+    // arguments: Launch config
+    def parseData(arguments: JsonObject) =
+      Option(arguments.getAsJsonPrimitive("data"))
+        .toRight("missing 'data' field from launch request")
+        .flatMap(path =>
+          Either
+            .catchNonFatal(Paths.get(path.getAsString))
+            .leftMap(t => s"'data' field from launch request is not a valid 
path: $t")
+            .ensureOr(path => s"data file at $path doesn't 
exist")(_.toFile().exists())
+        )
+        .toEitherNel
+
+    // Parse the stopOnEntry field from the launch config
+    // Defaults to true
+    //
+    // arguments: Launch config
+    def parseStopOnEntry(arguments: JsonObject) =
+      Option(arguments.getAsJsonPrimitive("stopOnEntry"))
+        .map(_.getAsBoolean())
+        .getOrElse(true)
+        .asRight[String]
+        .toEitherNel
+
+    // Parse the infosetOutput object from the launch config
+    //
+    // infosetOutput: {
+    //   type: '',
+    //   path: ''
+    // }
+    //
+    // Type must be 'none' | 'console' | 'file'
+    // If type is 'file', there must be a 'path' field that contains a valid 
path
+    //   for the resulting infoset to be written to
+    // A case where the user expects a new directory to be created is not a 
valid path
+    //   eg. /path/to/<existing>/<non-existing>/file.tdml
+    //
+    // arguments:   Launch config
+    // requireFile: Whether or not the type field must be set to file. This is 
a requirement
+    //              for the TDML generate operation. Returns an error if this 
boolean
+    //              is set to True, and the type field is set to a value other 
than file
+    def parseInfosetOutput(arguments: JsonObject, requireFile: Boolean = 
false) =
+      Option(arguments.getAsJsonObject("infosetOutput")) match {
+        case None => Right(LaunchArgs.InfosetOutput.Console).toEitherNel
+        case Some(infosetOutput) =>
+          Option(infosetOutput.getAsJsonPrimitive("type")) match {
             case None => Right(LaunchArgs.InfosetOutput.Console).toEitherNel
-            case Some(infosetOutput) =>
-              Option(infosetOutput.getAsJsonPrimitive("type")) match {
-                case None => 
Right(LaunchArgs.InfosetOutput.Console).toEitherNel
-                case Some(typ) =>
-                  typ.getAsString() match {
-                    case "none"    => 
Right(LaunchArgs.InfosetOutput.None).toEitherNel
-                    case "console" => 
Right(LaunchArgs.InfosetOutput.Console).toEitherNel
-                    case "file" =>
-                      Option(infosetOutput.getAsJsonPrimitive("path"))
-                        .toRight("missing 'infosetOutput.path' field from 
launch request")
-                        .flatMap(path =>
-                          Either
-                            
.catchNonFatal(LaunchArgs.InfosetOutput.File(Paths.get(path.getAsString)))
-                            .leftMap(t => s"'infosetOutput.path' field from 
launch request is not a valid path: $t")
-                            .ensureOr(file => s"can't write to infoset output 
file at ${file.path}") { f =>
-                              val file = f.path.toFile
-                              file.canWrite || (!file.exists && 
file.getParentFile.canWrite)
-                            }
-                        )
-                        .toEitherNel
-                    case invalidType =>
-                      Left(
-                        s"invalid 'infosetOutput.type': '$invalidType', must 
be 'none', 'console', or 'file'"
-                      ).toEitherNel
-                  }
+            case Some(typ) =>
+              typ.getAsString() match {
+                case "none" =>
+                  if (requireFile)
+                    Left("'type' field in 'infosetOutput' must be set to 
'file'").toEitherNel
+                  else
+                    Right(LaunchArgs.InfosetOutput.None).toEitherNel
+                case "console" =>
+                  if (requireFile)
+                    Left("'type' field in 'infosetOutput' must be set to 
'file'").toEitherNel
+                  else
+                    Right(LaunchArgs.InfosetOutput.Console).toEitherNel
+                case "file" =>
+                  Option(infosetOutput.getAsJsonPrimitive("path"))
+                    .toRight("missing 'infosetOutput.path' field from launch 
request")
+                    .flatMap(path =>
+                      Either
+                        
.catchNonFatal(LaunchArgs.InfosetOutput.File(Paths.get(path.getAsString)))
+                        .leftMap(t => s"'infosetOutput.path' field from launch 
request is not a valid path: $t")
+                        .ensureOr(file => s"can't write to infoset output file 
at ${file.path}") { f =>
+                          val file = f.path.toFile
+                          // If an empty string is passed in, it will be set 
to the workspace directory by default
+                          // This is inside the Java code, so we have to make 
sure that the TDML file we
+                          //   are working with is not a directory
+                          !file.isDirectory() && (file.canWrite || 
(!file.exists && file.getParentFile.canWrite))
+                        }
+                    )
+                    .toEitherNel
+                case invalidType =>
+                  Left(
+                    s"invalid 'infosetOutput.type': '$invalidType', must be 
'none', 'console', or 'file'"
+                  ).toEitherNel
               }
           }
-        ).parMapN(LaunchArgs.apply)
-    }
+      }
+
+    // The following functions granularly parse the tdmlConfig object from the 
launch config
+    //
+    // tdmlConfig: {
+    //   action: '',
+    //   name: '',
+    //   description: '',
+    //   path: ''
+    // }
+    //
+    // The action field is parsed elsewhere. If these functions are hit, a 
valid action
+    //   other than 'none' was found.
+
+    // Parse the  name field from the tdmlConfig object from the launch config
+    // Returns an error if the field is missing or is an empty string
+    //
+    // tdmlConfig: tdmlConfig object from the launch config
+    def parseTDMLName(tdmlConfig: JsonObject) =
+      Option(tdmlConfig.getAsJsonPrimitive("name"))
+        .toRight("missing 'tdmlConfig.name' field from launch request")
+        .map(_.getAsString())
+        .flatMap(name => Either.cond(name.length() > 0, name, "'name' field 
from 'tdmlConfig' object cannot be empty"))
+        .toEitherNel
+
+    // Parse the description field from the tdmlConfig object from the launch 
config
+    // Returns an error if the field is missing or is an empty string
+    //
+    // tdmlConfig: tdmlConfig object from the launch config
+    def parseTDMLDescription(tdmlConfig: JsonObject) =
+      Option(tdmlConfig.getAsJsonPrimitive("description"))
+        .toRight("missing 'tdmlConfig.description' field from launch request")
+        .map(_.getAsString())
+        .flatMap(description =>
+          Either
+            .cond(description.length() > 0, description, "'description' field 
from 'tdmlConfig' object cannot be empty")
+        )
+        .toEitherNel
+
+    // Parse the path field from the tdmlConfig object from the launch config
+    // Returns an error if the field is missing or is an invalid path
+    // A case where the user expects a new directory to be created is not a 
valid path
+    //   eg. /path/to/<existing>/<non-existing>/file.tdml
+    //
+    // tdmlConfig: tdmlConfig object from the launch config
+    def parseTDMLPath(tdmlConfig: JsonObject) =
+      Option(tdmlConfig.getAsJsonPrimitive("path"))
+        .toRight("missing 'tdmlConfig.path' field from launch request")
+        .flatMap(path =>
+          Either
+            
.catchNonFatal(Paths.get(path.getAsString).toFile().getAbsolutePath())
+            .leftMap(t => s"'infosetOutput.path' field from launch request is 
not a valid path: $t")
+            .ensureOr(file => s"can't write to infoset output file at 
${file}") { f =>
+              val file = Paths.get(f).toFile()
+              // If an empty string is passed in, it will be set to the 
workspace directory by default
+              // This is inside the Java code, so we have to make sure that 
the TDML file we
+              //   are working with is not a directory
+              !file.isDirectory() && (file.canWrite || (!file.exists && 
file.getParentFile.canWrite))
+            }
+        )
+        .toEitherNel

Review Comment:
   The intent of this feature was just to implement very basic TDML test cases 
into the extension. I agree that the code should not be duplicated. I'm hoping 
that someone will be willing to come along to refactor this to reference the 
TDML code in the main daffodil repo (either directly, or refactor it into a 
separate library), but that seemed out of scope for this implementation.
   
   The main use case for this feature as it exists now is for a user of the 
extension to use the 'Generate TDML' command to create a TDML file that can be 
added to a DFDL schema and an input file, and sent to the user list for 
debugging purposes. Then, someone should be able to take those files and 
execute the test case from the TDML file to get the same execution as the 
original poster.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to