From 9db7d96a065cf19a23a238c1dbb75fd5a5382660 Mon Sep 17 00:00:00 2001 From: perro Date: Thu, 26 Jan 2023 18:58:23 -0800 Subject: [PATCH] =?UTF-8?q?Implementaci=C3=B3n=20de=20'=5F=5Fbuild=5Feleme?= =?UTF-8?q?nts'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 52 +++++----- yasd.py | 282 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 211 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 082c078..2bc3511 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ To better understad what YASD does, see the inputs and outputs: elements: - ELMT ... - attributes: + attributeElements: - ATTR ... groups: @@ -71,13 +71,9 @@ To better understad what YASD does, see the inputs and outputs: description: Element description type: simple|empty|no_text|no_elements|mixed datatype: string|integer|decimal|date|time|language|duration|token|boolean|byte|int|double|float|long|short|normalizedString|dateTime|gDay|gMonth|gMonthDay|gYear|gYearMonth|negativeInteger|nonNegativeInteger|nonPositiveInteger|positiveInteger|unsignedLong|unsignedInt|unsignedShort|unsignedByte|anyURI|base64Binary|hexBinary|Name|QName|NCName|ID|IDREF|IDREFS|ENTITY|ENTITIES|NMTOKEN|NMTOKENS|NOTATION - default: a_value - fixed: a_value - restriction: - CONSTRAIN - ... - attribute: - - attribute_name + attributes: + - name: attribute_name + - group: group_name ... children_order: all|choice|sequence children: @@ -96,7 +92,7 @@ To better understad what YASD does, see the inputs and outputs: fixed: a_value use: required restriction: - CONSTRAIN + - CONSTRAIN ... ### Group (GRPS) structure @@ -176,19 +172,19 @@ Allowed types: - `simple`. Only text node allowed. - `empty`. Only attributes allowed. -- `no_text`. No text nodes allowed. +- `no_text`. No children text nodes allowed. - `no_elements`. No children elements allowed. - `mixed`. Children elements, text node and attributes allowed. Chart: -| type | children elements | text node | attributes | -|------------|:-----------------:|:---------:|:----------:| -|simple | ✗ | ✓ | ✗ | -|empty | ✗ | ✗ | ✓ | -|no_text | ✓ | ✗ | ✓ | -|no_elements | ✗ | ✓ | ✓ | -|mixed | ✓ | ✓ | ✓ | +| type | children elements | children text node | attributes | +|------------|:-----------------:|:------------------:|:----------:| +|simple | ✗ | ✓ | ✗ | +|empty | ✗ | ✗ | ✓ | +|no_text | ✓ | ✗ | ✓ | +|no_elements | ✗ | ✓ | ✓ | +|mixed | ✓ | ✓ | ✓ | > **Note**: attributes are never mandatory; they could be zero or more. @@ -268,11 +264,15 @@ Indicates default value when element or attribute is empty. Optional. +Only allowed for simple elements or attributes. + ### `fixed` Indicates fixed value to element or attribute. -Optional. +Optional; ignored if 'default' is present. + +Only allowed for simple elements or attributes. ### `use` @@ -284,12 +284,10 @@ Only `required` is valid as value. ### `restriction` -Indicates accepted constrained values for element or attribute. +Indicates accepted constrained values for attribute. Optional; if present, must contain at least one constrain. -Not allowed for 'empty' and 'no_text' elements. - Allowed constrains: - `enumeration`. Specifies a list of acceptable values. @@ -316,7 +314,7 @@ Allowed constrains: carriage returns) is handled; accepted values are `preserve|replace|collapse`. -### `attribute` +### `attributes` Indicates a list of attributes for an element. @@ -384,9 +382,9 @@ Indicates elements for schema. Mandatory. -### `attributes` +### `attributeElements` -Indicates attributes for schema. +Indicates attributes elements for schema. Optional. @@ -398,11 +396,13 @@ Mandatory. ## References -* “XSD Tutorial”, [Tutorials Point]. +* “XML Schema Reference”, [W3ref] * “XML Schema Tutorial”, [W3Schools]. +* “XSD Tutorial”, [Tutorials Point]. [YASD]: https://gitlab.com/amlengua/apal/esquema/-/blob/main/apal.yaml [XSD]: https://gitlab.com/amlengua/apal/esquema/-/blob/main/apal.xsd [RST]: https://gitlab.com/amlengua/apal/esquema/-/blob/main/apal.rst - [Tutorials Point]: https://www.tutorialspoint.com/xsd/ + [W3ref]: https://www.w3schools.com/xml/schema_elements_ref.asp [W3Schools]: https://www.w3schools.com/xml/schema_intro.asp + [Tutorials Point]: https://www.tutorialspoint.com/xsd/ diff --git a/yasd.py b/yasd.py index f821bf9..7ca8390 100755 --- a/yasd.py +++ b/yasd.py @@ -57,8 +57,8 @@ class YASD: self.msgr = YASDMessenger(quiet=quiet, log=log) self.yaml = YASDCheck(indata, self.msgr).yaml self.formatter = XMLFormatter(indent=2) - self.soups = self.__get_soups() self.outfile = outfile + self.xsd = None self.out = "" def convert(self, stdout=False): @@ -71,6 +71,7 @@ class YASD: self.__build_schema() self.__build_elements() self.__build_attributes() + self.__build_groups() self.__stringify_xsd() if stdout: self.__output() @@ -84,8 +85,8 @@ class YASD: :param stdout: if sample goes to stdout or not; 'False' by default :type stdout: True or False """ - # TODO: XML sample - self.out = "TODO: XML sample" + # TODO XML sample + self.out = "XML sample" if stdout: self.__output() else: @@ -93,73 +94,69 @@ class YASD: def document(self, stdout=False): """ - Generates RST documentation + Generates RST documentation. :param stdout: if document goes to stdout or not; 'False' by default :type stdout: True or False """ - # TODO: RST document - self.out = f"TODO: RST document from :{self.__dict__}" + # TODO RST document + self.out = f"RST document from :{self.__dict__}" if stdout: self.__output() else: return self.out def __build_schema(self): - unwanted = "version schemaLocation".split() - for key in unwanted: - self.yaml["schema"].pop(key) - soup = BeautifulSoup(parser="xml") - schema = soup.new_tag("schema", nsprefix="xs") + """ + Builds root node for XSD. + """ + for key in ["version", "schemaLocation"]: + del self.yaml["schema"][key] + self.xsd = BeautifulSoup(parser="xml") + schema = self.xsd.new_tag("schema", nsprefix="xs") schema["xmlns:xs"] = "http://www.w3.org/2001/XMLSchema" - soup.append(schema) for key, val in self.yaml["schema"].items(): schema[key] = val - self.soups["schema"] = soup + self.xsd.append(schema) def __build_elements(self): - soup = BeautifulSoup(parser="xml") + """ + Builds element nodes for XSD. + + Element nodes can be simple or complex types. + """ for el in self.yaml["elements"]: el = self.__sanitize(el) - { - "simple": self.__build_element_simple(soup, el), - "empty": self.__build_element_empty(soup, el), - "no_text": self.__build_element_no_text(soup, el), - "no_elements": self.__build_element_no_elements(soup, el), - "mixed": self.__build_element_mixed(soup, el), - }[el["type"]] - if len(soup.contents) > 0: - print(len(soup.contents)) - self.soups["elements"] = soup - - def __build_element_simple(self, main_soup, el): - # element = self.__build_simple(el) - # main_soup.append(element) - ... - - def __build_element_empty(self, main_soup, el): - ... - - def __build_element_no_text(self, main_soup, el): - ... - - def __build_element_no_elements(self, main_soup, el): - ... - - def __build_element_mixed(self, main_soup, el): - ... + if el["type"] == "simple": + self.__build_simple(el) + else: + self.__build_complex(el) def __build_attributes(self): - soup = BeautifulSoup(parser="xml") - for el in self.yaml["attributes"]: - element = self.__build_simple(self.__sanitize(el), tag="attribute") - soup.append(element) - self.soups["attributes"] = soup + """ + Builds attributes nodes for XSD. + + Attributes are always simple types. + """ + for el in self.yaml["attributeElements"]: + self.__build_simple(self.__sanitize(el), tag="attribute") + + def __build_groups(self): + # TODO build groups + ... def __build_simple(self, el, tag="element"): - soup = BeautifulSoup(parser="xml") - element = soup.new_tag(tag, nsprefix="xs") - soup.append(element) + """ + Builds simple node for XSD. + + :param dict el: YASD element + :param str tag: tag name for XSD node + """ + # TODO fix according to 'attribute', 'element' and 'simpleType' refs + # https://www.w3schools.com/xml/schema_elements_ref.asp + element = self.xsd.new_tag(tag, nsprefix="xs") + if "default" in el.keys() and "fixed" in el.keys(): + del el["fixed"] for key, val in el.items(): if key == "datatype": element["type"] = f"xs:{val}" @@ -167,18 +164,140 @@ class YASD: self.__build_restriction(element, val) else: element[key] = val - return soup + self.xsd.schema.append(element) - def __build_restriction(self, root, elements): - soup = BeautifulSoup(parser="xml") - simple_type = soup.new_tag("simpleType", nsprefix="xs") - restriction = soup.new_tag("restriction", nsprefix="xs") - restriction["base"] = self.__get_base(list(elements.keys())[0]) - for key, val in elements.items(): - constrain = soup.new_tag(key, nsprefix="xs", value=val) - restriction.append(constrain) - simple_type.append(restriction) - root.append(simple_type) + def __build_complex(self, el): + """ + Builds complex node for XSD. + + :param dict el: YASD element + """ + # TODO fix according to 'element' and 'complexType' refs + # https://www.w3schools.com/xml/schema_elements_ref.asp + element = self.__build_complex_root(el) + 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) + complex_type.append(indicator) + element.append(complex_type) + self.xsd.schema.append(element) + + def __build_complex_root(self, el): + """ + Builds root complex node for XSD. + + :param dict el: YASD element + :return: root complex node + :rtype: bs4.element.Tag + """ + element = self.xsd.new_tag("element", nsprefix="xs") + element["name"] = el["name"] + return element + + def __build_complex_type(self, el): + """ + Builds complex type node for XSD. + + :param dict el: YASD element + :return: root complex node + :rtype: bs4.element.Tag + """ + container = self.xsd.new_tag("complexType", nsprefix="xs") + simple_content = self.__build_simple_content(el) + if simple_content is not None: + container.append(simple_content) + if el["type"] == "mixed": + container["mixed"] = "true" + return container + + def __build_simple_content(self, el): + """ + Builds simple content node for XSD. + """ + simple_content = None + if el["type"] == "no_elements": + simple_content = self.xsd.new_tag("simpleContent", nsprefix="xs") + extension = self.xsd.new_tag("extension", nsprefix="xs") + extension["base"] = f"xs:{el['datatype']}" + self.__add_references(extension, el, is_attr=True) + simple_content.append(extension) + return simple_content + + def __build_restriction(self, root, restrs, simple=True): + """ + Builds restriction node for XSD. + + :param bs4.element.Tag root: root node that requires restriction node + :param dict restrs: restrictions for root node + :param str container_tag: name of container tag for restriction + """ + if simple: + container = self.xsd.new_tag("simpleType", nsprefix="xs") + else: + container = self.xsd.new_tag("complexContent", nsprefix="xs") + restriction = self.xsd.new_tag("restriction", nsprefix="xs") + restriction["base"] = self.__get_base(restrs) + for restr in restrs: + for key, val in restr.items(): + constrain = self.xsd.new_tag(key, nsprefix="xs", value=val) + restriction.append(constrain) + container.append(restriction) + root.append(container) + + def __get_base(self, restrictions): + """ + Gets restriction data type. + + It uses the first restriction to get the data type. A valid restriction + node always have the same data type for all its restrictions. + + :param dict restrictions: restrictions as a dict + :return: 'xs:string' or 'xs:integer' + :rtype: str + """ + key = list(restrictions[0].keys())[0] + strings = "enumeration pattern whiteSpace length minLength maxLength" + if key in strings.split(): + return "xs:string" + else: + return "xs:integer" + + def __get_references(self, el, is_attr): + """ + Gets required variables values for references. + + :param dict el: YASD element + :param is_attr: if is and attribute reference + :type is_attr: True or False + """ + key, tag = "children", "element" + if is_attr: + key, tag = "attributes", "attribute" + if key in el.keys() and "group" in el[key][0].keys(): + tag, name = "group", "group" + else: + name = "name" + return key, tag, name + + def __add_references(self, root, el, is_attr=False): + """ + Adds element or attribute references to root node. + + :param bs4.element.Tag root: root node that requires references + :param dict el: YASD element + :param is_attr: if is an attribute reference; 'False' by default + :type is_attr: True or False + """ + key, tag, name = self.__get_references(el, is_attr) + if key in el.keys(): + for element in el[key]: + node = self.xsd.new_tag(tag, nsprefix="xs") + node["ref"] = element[name] + root.append(node) + del el[key] def __sanitize(self, el): """ @@ -194,40 +313,11 @@ 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. - - :param str key: Type of restriction - :return: 'xs:string' or 'xs:integer' - :rtype: str - """ - strings = "enumeration pattern whiteSpace length minLength maxLength" - if key in strings.split(): - return "xs:string" - else: - return "xs:integer" - def __stringify_xsd(self): """ Converts BeautifulSoups to pretty text format. """ - xsd = self.soups["schema"] - del self.soups["schema"] - for key, val in self.soups.items(): - xsd.append(val) - self.out = xsd.prettify(formatter=self.formatter) + self.out = self.xsd.prettify(formatter=self.formatter) def __output(self, extname=".xsd"): """ @@ -301,7 +391,7 @@ class YASDCheck: :return: YASD structure :rtype: dict """ - # TODO: extra checks for self.yaml + # TODO extra checks for self.yaml ... @@ -313,10 +403,8 @@ class YASDMessenger: def keys(): """ Messages keys dictionary. - - Here multilang support could be implemented with: - https://github.com/sectasy0/pyi18n """ + # TODO internationalization with: https://github.com/sectasy0/pyi18n return { "description": """ YASD, Yet Another Schema Definition. YASD is a YAML format for @@ -363,7 +451,7 @@ class YASDMessenger: self.__check_level(level) msg = self.__get_msg(key, **kwargs) msg = f"[{level.upper()}] {msg}" - # TODO: print or save depending on self.quiet and self.log + # TODO print or save depending on self.quiet and self.log print(msg) if level in ["error", "fatal"]: sys.exit(1) @@ -410,8 +498,8 @@ class YASDCLI: self.__init_parser() args = self.parser.parse_args() if args.action == "man": - # TODO: print man from README - print("TODO: MAN") + # TODO print man from README + print("Manual") else: YASD.do(args.action, args.input, args.output, args.quiet, args.log)