Re: Split the Root: a little design pattern

by Finnian Reilly (modified: 2018 Mar 17)

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.