Re: Split the Root: a little design pattern

by Finnian Reilly (modified: 2018 Mar 17)

    Contents
  1. Introduction
    1. Splitting the Root
  2. Command Argument Mapping
    1. Argument Validation

Introduction

If you don't mind the fact that it is not void safe, Eiffel-Loop has a useful implementation of the "split the root" design pattern discussed in Prof. Meyer's technology blog. To which I have added a comment.

What is interesting about the Eiffel-Loop implementation is it offers an implicit way of mapping command line arguments to the make routine arguments of a core application object implementing EL_COMMAND, so you don't need to query the command line arguments directly. This is achieved via class EL_COMMAND_LINE_SUB_APPLICATION which takes a generic argument conforming to EL_COMMAND. The latter corresponds to the "CORE" class in Prof. Meyer's article.

Splitting the Root

Eiffel-Loop supports the philosophy that it's more useful (and convenient) to have a command line application that does a number of related (or unrelated things). A typical example is the the Eiffel-Loop toolkit application el_toolkit. Each of these sub-applications is reachable by a command line switch defined by {EL_SUB_APPLICATION}.option_name which defaults to generator.as_lower unless you redefine it with a short name.

class APPLICATION_ROOT inherit EL_MULTI_APPLICATION_ROOT [BUILD_INFO] create make feature {NONE} -- Implementation Application_types: ARRAY [TYPE [EL_SUB_APPLICATION]] -- once Result := << {AUTOTEST_DEVELOPMENT_APP}, {UNDATED_PHOTOS_APP}, {CRYPTO_APP}, {FILTER_INVALID_UTF_8_APP}, {FTP_BACKUP_APP}, -- uses ftp (depends eposix) {HTML_BODY_WORD_COUNTER_APP}, {JOBSERVE_SEARCH_APP}, {PRAAT_GCC_SOURCE_TO_MSVC_CONVERTOR_APP}, {PYXIS_ENCRYPTER_APP}, {PYXIS_TO_XML_APP}, {PYXIS_TREE_TO_XML_COMPILER_APP}, {LOCALIZATION_COMMAND_SHELL_APP}, {PYXIS_TRANSLATION_TREE_COMPILER_APP}, {THUNDERBIRD_LOCALIZED_HTML_EXPORTER_APP}, {THUNDERBIRD_WWW_EXPORTER_APP}, {VCF_CONTACT_SPLITTER_APP}, {VCF_CONTACT_NAME_SWITCHER_APP}, {XML_TO_PYXIS_APP}, {YOUTUBE_HD_DOWNLOAD_APP} >> end end

Command Argument Mapping

The class EL_COMMAND_LINE_SUB_APPLICATION (a descendant of EL_SUB_APPLICATION) takes a generic parameter conforming to class EL_COMMAND. By implementing the function default_make: PROCEDURE you can implicitly map command line argument to the arguments for the make routine for the class implementing EL_COMMAND. Take for example the following code fragment for class THUNDERBIRD_LOCALIZED_HTML_EXPORTER which has a make routine taking 5 arguments: note description: "Export Thunderbird email client HTML as XHTML for selected folders" class THUNDERBIRD_LOCALIZED_HTML_EXPORTER inherit THUNDERBIRD_EXPORTER rename make as make_exporter end EL_COMMAND create make feature {EL_SUB_APPLICATION} -- Initialization make ( a_account_name: ZSTRING; a_export_path, thunderbird_home_dir: EL_DIR_PATH a_is_xhtml: BOOLEAN; a_included_folders: like included_folders ) do make_exporter (a_account_name, a_export_path, thunderbird_home_dir) is_xhtml := a_is_xhtml; included_folders := a_included_folders included_folders.compare_objects end feature {NONE} -- Internal attributes is_xhtml: BOOLEAN included_folders: EL_ZSTRING_LIST -- .sbd folders end

These make arguments are mapped to command line arguments in the class THUNDERBIRD_LOCALIZED_HTML_EXPORTER_APP as shown below. The routine default_make provides default arguments for {THUNDERBIRD_LOCALIZED_HTML_EXPORTER}.make which are over-ridden by command line arguments defined by the function argument_specs. The class EL_REGRESSION_TESTABLE_COMMAND_LINE_SUB_APPLICATION is just a variant of class EL_COMMAND_LINE_SUB_APPLICATION that provides some regressions testing capabilities during development and is triggered by the command line switch -test. The various descriptions found in argument_specs and Description are for the benefit of the "quick help" mode invoked by the command switch -help.

class THUNDERBIRD_LOCALIZED_HTML_EXPORTER_APP inherit EL_REGRESSION_TESTABLE_COMMAND_LINE_SUB_APPLICATION [THUNDERBIRD_LOCALIZED_HTML_EXPORTER] redefine Option_name end create make feature -- Test test_run -- do -- Test.do_file_tree_test (".thunderbird", agent test_xhtml_export ("pop.myching.co", ?), 2477712861) -- Test.do_file_tree_test (".thunderbird", agent test_xhtml_export ("small.myching.co", ?), 4123295270) Test.do_file_tree_test (".thunderbird", agent test_html_body_export ("pop.myching.co", ?), 2383008038) -- Test.do_file_tree_test (".thunderbird", agent test_html_body_export ("small.myching.co", ?), 4015841579) end test_xhtml_export (account: ZSTRING; a_dir_path: EL_DIR_PATH) -- do create command.make ( account, a_dir_path.joined_dir_path ("export"), a_dir_path.parent, True, Empty_inluded_sbd_dirs ) normal_run end test_html_body_export (account: ZSTRING; a_dir_path: EL_DIR_PATH) -- local en_file_path: EL_FILE_PATH; en_text, subject_line: STRING; en_out: PLAIN_TEXT_FILE pos_subject: INTEGER do create command.make ( account, a_dir_path.joined_dir_path ("export"), a_dir_path.parent, False, Empty_inluded_sbd_dirs ) normal_run -- Change name of "Home" to "Home Page" en_file_path := a_dir_path + "21h18lg7.default/Mail/pop.myching.co/Product Tour.sbd/en" en_text := File_system.plain_text (en_file_path) subject_line := "Subject: Home" pos_subject := en_text.substring_index (subject_line, 1) if pos_subject > 0 then en_text.replace_substring (subject_line + " Page", pos_subject, pos_subject + subject_line.count - 1) end create en_out.make_open_write (en_file_path) en_out.put_string (en_text) en_out.close normal_run end feature {NONE} -- Implementation argument_specs: ARRAY [like specs.item] do Result := << required_argument ("account", "Thunderbird account name"), required_argument ("output", "Output directory path"), optional_argument ("thunderbird_home", "Location of .thunderbird"), optional_argument ("as_xhtml", "Export as xhtml"), optional_argument ("folders", "Folders to include") >> end default_make: PROCEDURE do Result := agent {like command}.make ("", "", Directory.Home, False, create {EL_ZSTRING_LIST}.make (7)) end feature {NONE} -- Constants Empty_inluded_sbd_dirs: EL_ZSTRING_LIST once create Result.make (0) end Option_name: STRING = "export_thunderbird" Description: STRING = "Export multi-lingual HTML content from Thunderbird" Log_filter: ARRAY [like CLASS_ROUTINES] -- do Result := << [{THUNDERBIRD_LOCALIZED_HTML_EXPORTER_APP}, All_routines], [{THUNDERBIRD_LOCALIZED_HTML_EXPORTER}, All_routines], [{THUNDERBIRD_EXPORT_AS_XHTML}, All_routines], [{THUNDERBIRD_EXPORT_AS_XHTML_BODY}, All_routines] >> end end
The full range of make arguments which can be mapped to command line arguments is defined by the following hierarchy of agent operand setter classes: EL_MAKE_OPERAND_SETTER* [G] EL_BOOLEAN_OPERAND_SETTER EL_ZSTRING_OPERAND_SETTER EL_STRING_8_OPERAND_SETTER EL_STRING_32_OPERAND_SETTER EL_ZSTRING_TABLE_OPERAND_SETTER EL_INTEGER_OPERAND_SETTER EL_ENVIRON_VARIABLE_OPERAND_SETTER [E -> EL_ENVIRON_VARIABLE create make_from_string end] EL_NATURAL_OPERAND_SETTER EL_INTEGER_64_OPERAND_SETTER EL_NATURAL_64_OPERAND_SETTER EL_REAL_OPERAND_SETTER EL_DOUBLE_OPERAND_SETTER EL_PATH_OPERAND_SETTER* [G -> EL_PATH] EL_FILE_PATH_OPERAND_SETTER EL_BUILDABLE_FROM_FILE_OPERAND_SETTER EL_DIR_PATH_OPERAND_SETTER

To see how these classes are used see class EL_COMMAND_ARGUMENT. The once table constant Setter_types defines a mapping between a make argument and it's command line setter. Looking at the routine set_operand you will notice that you can also map chains of these basic types to a single command line argument. elseif attached {CHAIN [ANY]} operand as list then if list.generating_type.generic_parameter_count = 1 then Setter_types.search (list.generating_type.generic_parameter_type (1))

You can for example have an argument number: ARRAYED_LIST [INTEGER]. This will be mapped to a command line argument because it conforms to the type CHAIN [INTEGER].

Argument Validation

A system of agent based argument validation is also supported by using either of the functions valid_required_argument or valid_optional_argument when implementing the function argument_specs. Here is an example from class PYXIS_TO_XML_APP. Note that because the final argument to valid_required_argument is an array, you can add as many validators as you wish. feature {NONE} -- Implementation argument_specs: ARRAY [like specs.item] do Result := << valid_required_argument ("in", "Input file path", << file_must_exist >>), optional_argument ("out", "Output file path") >> end
file_must_exist is an "out of the box" validator, which can serve as an example for defining your own. Here is the complete list of "out of the box" validators: feature {NONE} -- Validations always_valid: TUPLE [key: READABLE_STRING_GENERAL; value: PREDICATE] do Result := ["Always true", agent: BOOLEAN do Result := True end] end file_must_exist: like always_valid do Result := [ "The file must exist", agent (path: EL_FILE_PATH): BOOLEAN do Result := not path.is_empty implies path.exists end ] end directory_must_exist: like always_valid do Result := [ "The directory must exist", agent (path: EL_DIR_PATH): BOOLEAN do Result := not path.is_empty implies path.exists end ] end

If the validation fails then the text description is displayed with a helpful error message for the bad argument.