From 5e21d1f3b3bb3c7a4aab85388ffab0f1f4b776e0 Mon Sep 17 00:00:00 2001 From: perro Date: Fri, 27 Jan 2023 19:58:35 -0800 Subject: [PATCH] Ya construye XSD aunque con errores --- README.md | 32 ++++++++------ yasd.py | 129 ++++++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 124 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 223ee90..1b10cf9 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,10 @@ To better understad what YASD does, see the inputs and outputs: ... children_order: all|choice|sequence children: - - name: element_name - maxOccurs: INTEGER + - ref: element_name + maxOccurs: INTEGER|unbounded minOccurs: INTEGER - - group: group_name + - group_ref: group_name ... ### Attribute (ATTR) structure @@ -97,7 +97,7 @@ To better understad what YASD does, see the inputs and outputs: attribute_group: true|false children_order: all|choice|sequence children: - - name: element_name + - ref: element_or_attribute_name maxOccurs: INTEGER|unbounded minOccurs: INTEGER @@ -137,7 +137,7 @@ Mandatory. ### `name` -Indicates element or attribute name. +Indicates element, attribute or group name. Mandatory. @@ -152,6 +152,12 @@ Naming rules: periods - Element names cannot contain spaces +### `ref` + +References element or attribute by name. + +Mandatory. + ### `description` Indicates element or attribute description in human readable form. @@ -174,13 +180,13 @@ Allowed types: Chart: - | type | elements | text | attributes | - |------------|:--------:|:----:|:----------:| - |simple | ✗ | ✓ | ✗ | - |empty | ✗ | ✗ | ✓ | - |no_text | ✓ | ✗ | ✓ | - |no_elements | ✗ | ✓ | ✓ | - |mixed | ✓ | ✓ | ✓ | + | type | elements | text | attributes | + |-------------|:--------:|:----:|:----------:| + | simple | ✗ | ✓ | ✗ | + | empty | ✗ | ✗ | ✓ | + | no_text | ✓ | ✗ | ✓ | + | no_elements | ✗ | ✓ | ✓ | + | mixed | ✓ | ✓ | ✓ | > **Note 1**: read "elements" and "text" with "direct children…" as prefix. @@ -353,7 +359,7 @@ Valid value is non negative integer. ### `group` -Indicates group name. +References group name. Optional. diff --git a/yasd.py b/yasd.py index aedf61c..e81bc4b 100755 --- a/yasd.py +++ b/yasd.py @@ -2,11 +2,12 @@ # (c) 2023 Perro Tuerto . # Founded by Mexican Academy of Language . # Licensed under GPLv3 . -# Requirements: python > 3.10, pyyaml, lxml, bs4, rich +# Requirements: python > 3.10, pyyaml, lxml, bs4, xmlschema, rich import sys import yaml import argparse +import xmlschema import urllib.request from pathlib import Path from bs4 import BeautifulSoup @@ -27,6 +28,7 @@ class YASD: quiet=False, log=False, stdout=False, + validate=False, ): """ Performs YASD actions directly. @@ -44,11 +46,13 @@ class YASD: :type log: True or False :param stdout: if conversion goes to stdout or not; 'False' by default :type stdout: True or False + :param validate: if XSD is validated; 'False' by default + :type validate: True or False :return: Output data; str on 'document'; bs4.element.Tag on 'convert' or 'sample'; YAML dict on 'check' :rtype: str or bs4.element.Tag or dict """ - yasd = YASD(indata, outfile, quiet, log, stdout) + yasd = YASD(indata, outfile, quiet, log, stdout, validate) yasd.msgr.run(f"action_{action}") match action: case "document": @@ -61,7 +65,13 @@ class YASD: return yasd.yaml def __init__( - self, indata=None, outfile=None, quiet=False, log=False, stdout=False + self, + indata=None, + outfile=None, + quiet=False, + log=False, + stdout=False, + validate=False, ): """ Inits YASD object. @@ -74,11 +84,16 @@ class YASD: :type quiet: True or False :param log: If messages are write in a file or not; 'False' by default :type log: True or False + :param stdout: if conversion goes to stdout or not; 'False' by default + :type stdout: True or False + :param validate: if XSD is validated; 'False' by default + :type validate: True or False """ self.msgr = YASDMessenger(quiet=quiet, log=log) self.yaml = YASDCheck(indata, self.msgr).yaml self.formatter = XMLFormatter(indent=2) self.stdout = stdout + self.validate = validate if outfile is None: self.outfile = None else: @@ -92,7 +107,11 @@ class YASD: :rtype: bs4.element.Tag """ self.xsd = YASDXSD(self.yaml, self.msgr).xsd - return self.__output(self.xsd) + out = self.__output(self.xsd) + if self.validate: + return YASDCheck.xsd(out, self.msgr) + else: + return out def sample(self): """ @@ -118,13 +137,12 @@ class YASD: """ Prints in the terminal or writes into a file. - :return: Output data; str if outdata comes from YASDRST; otherwise - bs4.element.Tag - :rtype: str or bs4.element.Tag + :return: Output data + :rtype: str """ + if type(outdata) is not str: + outdata = outdata.prettify(formatter=self.formatter) if self.stdout: - if type(outdata) is not str: - outdata = outdata.prettify(formatter=self.formatter) if self.outfile is None: print(outdata) else: @@ -184,7 +202,7 @@ class YASDXSD: def __build_attributes(self): """ - Builds attributes nodes for XSD. + Builds attribute nodes for XSD. Attributes are always simple types. """ @@ -192,8 +210,18 @@ class YASDXSD: self.__build_simple(self.__sanitize(el), tag="attribute") def __build_groups(self): - # TODO build groups - ... + """ + Builds group nodes for XSD. + """ + for el in self.yaml["groups"]: + if "attribute_group" in el.keys(): + # TODO build attributeGroup + ... + else: + element = self.xsd.new_tag("group", nsprefix="xs") + indicator = self.__build_indicator(el) + element.append(indicator) + self.xsd.schema.append(element) def __build_simple(self, el, tag="element"): """ @@ -224,9 +252,7 @@ class YASDXSD: complex_type = self.__build_complex_type(el) self.__add_references(complex_type, el, is_attr=True) if "children" in el.keys(): - other_tag = el["children_order"] - indicator = self.xsd.new_tag(other_tag, nsprefix="xs") - self.__add_references(indicator, el) + indicator = self.__build_indicator(el) complex_type.append(indicator) element.append(complex_type) self.xsd.schema.append(element) @@ -293,6 +319,17 @@ class YASDXSD: container.append(restriction) root.append(container) + def __build_indicator(self, el): + """ + Builds indicator node for XSD. + + :param dict el: YASD element + """ + other_tag = el["children_order"] + indicator = self.xsd.new_tag(other_tag, nsprefix="xs") + self.__add_references(indicator, el) + return indicator + def __get_base(self, restrictions): """ Gets restriction data type. @@ -325,7 +362,7 @@ class YASDXSD: if key in el.keys() and "group" in el[key][0].keys(): tag, name = "group", "group" else: - name = "name" + name = "ref" return key, tag, name def __add_references(self, root, el, is_attr=False): @@ -342,6 +379,10 @@ class YASDXSD: for element in el[key]: node = self.xsd.new_tag(tag, nsprefix="xs") node["ref"] = element[name] + if "maxOccurs" in element.keys(): + node["maxOccurs"] = element["maxOccurs"] + if "minOccurs" in element.keys(): + node["minOccurs"] = element["minOccurs"] root.append(node) del el[key] @@ -427,10 +468,12 @@ class YASDCheck: def file(filepath=None, msgr=None): """ - Verififes if file exists. + Verifies if file exists. :param filepath: File path :type filepath: None or Path + :param msgr: Messenger object + :type msgr: None or YASDMessenger """ msgr = YASDCheck.messenger(msgr) if type(filepath).__module__ != "pathlib": @@ -439,6 +482,40 @@ class YASDCheck: msgr.run("invalid_input", level="error", file=filepath) return filepath.resolve() + def url(key="readme", msgr=None): + """ + Verifies if remote file exists + + :param str key: YASDMessenger string key + :param msgr: Messenger object + :type msgr: None or YASDMessenger + """ + # TODO if YASD becomes pip package, the fetched files should be local + # Remove urllib import if that is the case + msgr = YASDCheck.messenger(msgr) + try: + url = YASDMessenger.keys()[key] + msgr.run("fetching", url=url) + return urllib.request.urlopen(url).read().decode("utf-8") + except Exception: + msgr.run("no_url", level="error", url=url) + + def xsd(xsd, msgr=None): + """ + Validates XSD. + + :param str xsd: XSD as XML string + :param msgr: Messenger object + :type msgr: None or YASDMessenger + """ + msgr = YASDCheck.messenger(msgr) + msgr.run("validating") + try: + xmlschema.XMLSchema(xsd) + except xmlschema.validators.exceptions.XMLSchemaParseError as error: + error = str(error).replace("\n", "\n ") + msgr.run("no_valid", error=error, level="error") + def __init__(self, indata=None, messenger=None): """ Inits YASD validator. @@ -502,20 +579,26 @@ class YASDMessenger: Academy of Language . Licensed under GPLv3 . """, + "readme": "https://gitlab.com/perrotuerto_personal/codigo/yasd/-/raw/no-masters/README.md", + "w3": "https://www.w3.org/2001/XMLSchema.xsd", "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_check": "checking YASD structure", "action_sample": "creating XML sample", "action_document": "creating RST documentation", "invalid_level": "invalid log level '@lvl'", "invalid_input": "invalid file '@file'", "invalid_yaml": "invalid YAML structure", - "no_yaml": "YAML dict needed", + "no_url": "failed to fetch '@url'", "no_input": "input file needed", + "no_yaml": "YAML dict needed", + "no_valid": "XSD schema has the following error:\n @error", + "fetching": "fetching '@url'", + "validating": "validating XSD schema", } def __init__(self, quiet=False, log=False): @@ -582,10 +665,7 @@ class YASDCLI: """ Prints README as manual. """ - # TODO if YASD becomes pip package, it should load local README - # Remove urllib import if that is the case - url = "https://gitlab.com/perrotuerto_personal/codigo/yasd/-/raw/no-masters/README.md" - raw = urllib.request.urlopen(url).read().decode("utf-8") + raw = YASDCheck.url() raw = raw.replace("## Table of Contents\n\n[TOC]\n\n", "") md = Markdown(raw) console = Console() @@ -607,7 +687,8 @@ class YASDCLI: args.output, args.quiet, args.log, - True, + stdout=True, + validate=True, ) def __init_parser(self):