diff --git a/README.md b/README.md index 6f430f6..082c078 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +--- +warn: This is just a prototype. +--- + # YASD, Yet Another Schema Definition YASD is a YAML format for human writable XSDs (XML Schema Definition), humans @@ -22,7 +26,7 @@ flowchart LR sample --- |from| YASD3([YASD]) YASD3 --> |to| XML([XML]) document --- |from| YASD4([YASD]) - YASD4 --> |to| MD([Markdown]) + YASD4 --> |to| RST([reStructuredText]) man --- |prints| README ``` @@ -32,7 +36,7 @@ To better understad what YASD does, see the inputs and outputs: - Input: human workable schema in [YASD]. - Output: computer processable schema in [XSD]. -- Output: human readable documentation in [Markdown]. +- Output: human readable documentation in [reStructuredText][RST]. ## Table of Contents @@ -399,6 +403,6 @@ Mandatory. [YASD]: https://gitlab.com/amlengua/apal/esquema/-/blob/main/apal.yaml [XSD]: https://gitlab.com/amlengua/apal/esquema/-/blob/main/apal.xsd - [Markdown]: https://gitlab.com/amlengua/apal/esquema/-/blob/main/apal.md + [RST]: https://gitlab.com/amlengua/apal/esquema/-/blob/main/apal.rst [Tutorials Point]: https://www.tutorialspoint.com/xsd/ [W3Schools]: https://www.w3schools.com/xml/schema_intro.asp diff --git a/yasd.py b/yasd.py index 8c4755e..bd10483 100755 --- a/yasd.py +++ b/yasd.py @@ -13,62 +13,95 @@ from bs4.formatter import XMLFormatter class YASD: """ - Performs YASD actions. + YASD actions performer. """ - def do(quiet, log, samples, action, filepath): - yasd = YASD(quiet, log, samples, filepath) - if action == "convert": - yasd.convert() - elif action == "sample": - yasd.sample() - elif action == "document": - yasd.document() + def do(action="check", indata=None, outfile=None, quiet=False, log=False): + """ + Performs YASD actions directly. - def __init__(self, quiet=False, log=False, samples=1, filepath=None): + Intented for YASDCLI, but can also be used programmatically. + + :param str action: YASD action to perform; 'check' by default + :param indata: YASD input; 'None' by default + :type indata: None or Path or dict + :param outfile: YASD output file path; 'None' by default + :type outfile: None or Path + :param quiet: If messages are print or not; 'False' by default + :type quiet: True or False + :param log: If messages are write in a file or not; 'False' by default + :type log: True or False + """ + yasd = YASD(indata, outfile, quiet, log) + yasd.msgr.run(f"action_{action}") + if action == "convert": + yasd.convert(stdout=True) + elif action == "sample": + yasd.sample(stdout=True) + elif action == "document": + yasd.document(stdout=True) + + def __init__(self, indata=None, outfile=None, quiet=False, log=False): """ Inits YASD object. + :param indata: YASD input; 'None' by default + :type indata: None or Path or dict + :param outfile: YASD output file path; 'None' by default + :type outfile: None or Path :param quiet: If messages are print or not; 'False' by default - :type quiet: False or True + :type quiet: True or False :param log: If messages are write in a file or not; 'False' by default - :type log: False or True - :param int samples: Quantity of XML samples; '1' by default - :param filepath: YASD file path; 'None' by default - :type filepath: None or Path + :type log: True or False """ self.msgr = YASDMessenger(quiet=quiet, log=log) - valid_input = YASDCheck(self.msgr, filepath) - self.filepath = valid_input.filepath - self.yaml = valid_input.yaml - self.samples = samples - self.soups = { - "schema": "", - "elements": "", - "attributes": "", - "groups": "", - } + self.yaml = YASDCheck(indata, self.msgr).yaml + self.formatter = XMLFormatter(indent=2) + self.soups = self.__get_soups() + self.outfile = outfile + self.out = "" - def convert(self): + def convert(self, stdout=False): """ Converts YASD to XSD. + + :param stdout: if conversion goes to stdout or not; 'False' by default + :type stdout: True or False """ self.__build_schema() self.__build_elements() self.__build_attributes() - self.__write() + self.__stringify_xsd() + if stdout: + self.__output() + else: + return self.out - def sample(self): + def sample(self, stdout=False): """ - Generates XML samples from YASD. - """ - print(f"TODO: {self.samples} samples") + Generates XML sample from YASD. - def document(self): + :param stdout: if sample goes to stdout or not; 'False' by default + :type stdout: True or False """ - Generates MD documentation + self.out = "TODO: XML sample" + if stdout: + self.__output() + else: + return self.out + + def document(self, stdout=False): """ - print("TODO: MD document from :", self.__dict__) + Generates RST documentation + + :param stdout: if document goes to stdout or not; 'False' by default + :type stdout: True or False + """ + self.out = f"TODO: RST document from :{self.__dict__}" + if stdout: + self.__output() + else: + return self.out def __build_schema(self): unwanted = "version schemaLocation".split() @@ -159,6 +192,17 @@ class YASD: del el["description"] return el + def __get_soups(self): + """ + Gets soups structures. + """ + return { + "schema": "", + "elements": "", + "attributes": "", + "groups": "", + } + def __get_base(self, key): """ Gets restriction data type. @@ -173,77 +217,129 @@ class YASD: else: return "xs:integer" - def __write(self): + def __stringify_xsd(self): """ - Writes XSD into a file. + Converts BeautifulSoups to pretty text format. """ - filename = Path(self.filepath.parent / f"{self.filepath.stem}.xsd") - formatter = XMLFormatter(indent=2) + xsd = self.soups["schema"] + del self.soups["schema"] for key, val in self.soups.items(): - if key == "schema": - xsd = val.schema - else: - xsd.append(val) - filename.write_text(xsd.prettify(formatter=formatter)) + xsd.append(val) + self.out = xsd.prettify(formatter=self.formatter) + + def __output(self, extname=".xsd"): + """ + Prints in the terminal or writes into a file. + """ + if self.outfile is None: + print(self.out) + else: + suffix = self.outfile.suffix + if len(suffix) > 0 and suffix == suffix.replace(" ", ""): + extname = suffix + filename = f"{self.outfile.stem}{extname}" + filename = Path(self.outfile.parent / filename) + filename.write_text(self.out) class YASDCheck: """ - Verifies YASD file. + YASD input validator. """ - def __init__(self, messenger, filepath): + def __init__(self, indata=None, messenger=None): """ Inits YASD validator. - :param YASDMessenger messenger: Object for print or save messages - :param filepath: YASD file path - :type filepath: None or Path + :param indata: YASD input + :type indata: None or Path or dict + :param messenger: Object for print or save messages + :type messenger: None or YASDMessenger """ - self.msgr = messenger - self.__check_file(filepath) - self.__parse_file() - # TODO: do extra checks + if messenger is None: + self.msgr = YASDMessenger() + else: + self.msgr = messenger + if type(indata) is dict: + self.yaml = indata + else: + self.yaml = self.parse_file(self.check_file(indata)) + self.check_structure() - def __check_file(self, filepath): + def check_file(self, filepath): """ Verifies YASD file. :param filepath: YASD file path :type filepath: None or Path """ - if filepath is None: + if type(filepath).__module__ != "pathlib": self.msgr.run("no_input", level="error") elif not filepath.exists() or not filepath.is_file(): self.msgr.run("invalid_input", level="error", file=filepath) - self.filepath = filepath.resolve() + return filepath.resolve() - def __parse_file(self): + def parse_file(self, filepath): """ Attempts YASD file parsing. + + :param filepath: YASD file path + :type filepath: Path """ - raw = self.filepath.read_text(encoding="utf8") + raw = filepath.read_text(encoding="utf8") try: - self.yaml = yaml.safe_load(raw) + return yaml.safe_load(raw) except yaml.YAMLError: - # TODO: should be log class self.msgr.run("invalid_yaml", level="error") + def check_structure(self): + """ + Verifies YASD structure. + + :return: YASD structure + :rtype: dict + """ + # TODO: extra checks for self.yaml + ... + class YASDMessenger: """ - Prints or saves YASD messages. + YASD printer or writer. """ def keys(): """ Messages keys dictionary. + + Here multilang support could be implemented with: + https://github.com/sectasy0/pyi18n """ return { + "description": """ + YASD, Yet Another Schema Definition. YASD is a YAML format for + human writable XSDs (XML Schema Definition), humans declare what is + indispensable, leaving the machines to do the rest of the + unreadable . + """, + "epilog": """ + (c) 2023 Perro Tuerto . Founded by Mexican + Academy of Language . Licensed under GPLv3 + . + """, + "help_action": "action to perform", + "help_input": "input file in YAML format", + "help_output": "output file", + "help_quiet": "enable quiet mode", + "help_log": "write log", + "action_convert": "Creating XSD schema", + "action_check": "Checking YASD", + "action_sample": "Creating XML sample", + "action_document": "Creating RST documentation", "invalid_level": "Invalid log level '@lvl'", - "no_input": "Input file needed.", "invalid_input": "Invalid file '@file'", "invalid_yaml": "Invalid YAML structure", + "no_input": "Input file needed.", } def __init__(self, quiet=False, log=False): @@ -278,7 +374,7 @@ class YASDMessenger: :param str level: Log level """ - if not level in ["trace", "debug", "info", "warn", "error", "fatal"]: + if level not in ["trace", "debug", "info", "warn", "error", "fatal"]: YASDMessenger().run("invalid_level", level="warn", lvl=level) def __get_msg(self, key, **kwargs): @@ -300,49 +396,59 @@ class YASDMessenger: return key -def main(): +class YASDCLI: """ - Gets and parses argv, then calls YASD. + YASD command-line interface. """ - parser = argparse.ArgumentParser( - prog="yasd", - description=""" - YASD, Yet Another Schema Definition. - YASD is a YAML format for human writable XSDs (XML Schema Definition), - humans declare what is indispensable, leaving the machines to do the - rest of the unreadable . - """, - epilog=""" - (c) 2023 Perro Tuerto . - Founded by Mexican Academy of Language . - Licensed under GPLv3 . - """, - ) - parser.add_argument( - "action", - choices=["convert", "check", "sample", "document", "man"], - help="action to perform", - ) - parser.add_argument( - "file", - type=Path, - nargs="?", - default=None, - help="file in YAML format", - ) - parser.add_argument( - "-q", "--quiet", action="store_true", help="enable quiet mode" - ) - parser.add_argument("-l", "--log", action="store_true", help="write log") - parser.add_argument( - "-n", "--num", default=1, help="number of XML samples; 1 by default" - ) - args = parser.parse_args() - if args.action == "man": - print("MAN") - else: - YASD.do(args.quiet, args.log, args.num, args.action, args.file) + + def __init__(self): + """ + Inits YASD CLI. + """ + self.__init_parser() + args = self.parser.parse_args() + if args.action == "man": + # TODO: print man from README + print("TODO: MAN") + else: + YASD.do(args.action, args.input, args.output, args.quiet, args.log) + + def __init_parser(self): + """ + Inits argument parser. + """ + msg = YASDMessenger.keys() + self.parser = argparse.ArgumentParser( + prog="yasd", + description=msg["description"], + epilog=msg["epilog"], + ) + self.parser.add_argument( + "action", + choices=["convert", "check", "sample", "document", "man"], + help=msg["help_action"], + ) + self.parser.add_argument( + "input", + type=Path, + nargs="?", + default=None, + help=msg["help_input"], + ) + self.parser.add_argument( + "-q", "--quiet", action="store_true", help=msg["help_quiet"] + ) + self.parser.add_argument( + "-l", "--log", action="store_true", help=msg["help_log"] + ) + self.parser.add_argument( + "-o", + "--output", + type=Path, + default=None, + help=msg["help_output"], + ) if __name__ == "__main__": - main() + YASDCLI()