Implementación de '__build_elements'

This commit is contained in:
perro tuerto 2023-01-26 18:58:23 -08:00
parent 0755e93695
commit 9db7d96a06
2 changed files with 211 additions and 123 deletions

View File

@ -51,7 +51,7 @@ To better understad what YASD does, see the inputs and outputs:
elements: elements:
- ELMT - ELMT
... ...
attributes: attributeElements:
- ATTR - ATTR
... ...
groups: groups:
@ -71,13 +71,9 @@ To better understad what YASD does, see the inputs and outputs:
description: Element description description: Element description
type: simple|empty|no_text|no_elements|mixed 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 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 attributes:
fixed: a_value - name: attribute_name
restriction: - group: group_name
CONSTRAIN
...
attribute:
- attribute_name
... ...
children_order: all|choice|sequence children_order: all|choice|sequence
children: children:
@ -96,7 +92,7 @@ To better understad what YASD does, see the inputs and outputs:
fixed: a_value fixed: a_value
use: required use: required
restriction: restriction:
CONSTRAIN - CONSTRAIN
... ...
### Group (GRPS) structure ### Group (GRPS) structure
@ -176,19 +172,19 @@ Allowed types:
- `simple`. Only text node allowed. - `simple`. Only text node allowed.
- `empty`. Only attributes allowed. - `empty`. Only attributes allowed.
- `no_text`. No text nodes allowed. - `no_text`. No children text nodes allowed.
- `no_elements`. No children elements allowed. - `no_elements`. No children elements allowed.
- `mixed`. Children elements, text node and attributes allowed. - `mixed`. Children elements, text node and attributes allowed.
Chart: Chart:
| type | children elements | text node | attributes | | type | children elements | children text node | attributes |
|------------|:-----------------:|:---------:|:----------:| |------------|:-----------------:|:------------------:|:----------:|
|simple | ✗ | ✓ | ✗ | |simple | ✗ | | ✗ |
|empty | ✗ | ✗ | ✓ | |empty | ✗ | | ✓ |
|no_text | ✓ | ✗ | ✓ | |no_text | ✓ | | ✓ |
|no_elements | ✗ | ✓ | ✓ | |no_elements | ✗ | | ✓ |
|mixed | ✓ | ✓ | ✓ | |mixed | ✓ | | ✓ |
> **Note**: attributes are never mandatory; they could be zero or more. > **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. Optional.
Only allowed for simple elements or attributes.
### `fixed` ### `fixed`
Indicates fixed value to element or attribute. Indicates fixed value to element or attribute.
Optional. Optional; ignored if 'default' is present.
Only allowed for simple elements or attributes.
### `use` ### `use`
@ -284,12 +284,10 @@ Only `required` is valid as value.
### `restriction` ### `restriction`
Indicates accepted constrained values for element or attribute. Indicates accepted constrained values for attribute.
Optional; if present, must contain at least one constrain. Optional; if present, must contain at least one constrain.
Not allowed for 'empty' and 'no_text' elements.
Allowed constrains: Allowed constrains:
- `enumeration`. Specifies a list of acceptable values. - `enumeration`. Specifies a list of acceptable values.
@ -316,7 +314,7 @@ Allowed constrains:
carriage returns) is handled; accepted values are carriage returns) is handled; accepted values are
`preserve|replace|collapse`. `preserve|replace|collapse`.
### `attribute` ### `attributes`
Indicates a list of attributes for an element. Indicates a list of attributes for an element.
@ -384,9 +382,9 @@ Indicates elements for schema.
Mandatory. Mandatory.
### `attributes` ### `attributeElements`
Indicates attributes for schema. Indicates attributes elements for schema.
Optional. Optional.
@ -398,11 +396,13 @@ Mandatory.
## References ## References
* “XSD Tutorial”, [Tutorials Point]. * “XML Schema Reference”, [W3ref]
* “XML Schema Tutorial”, [W3Schools]. * “XML Schema Tutorial”, [W3Schools].
* “XSD Tutorial”, [Tutorials Point].
[YASD]: https://gitlab.com/amlengua/apal/esquema/-/blob/main/apal.yaml [YASD]: https://gitlab.com/amlengua/apal/esquema/-/blob/main/apal.yaml
[XSD]: https://gitlab.com/amlengua/apal/esquema/-/blob/main/apal.xsd [XSD]: https://gitlab.com/amlengua/apal/esquema/-/blob/main/apal.xsd
[RST]: https://gitlab.com/amlengua/apal/esquema/-/blob/main/apal.rst [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 [W3Schools]: https://www.w3schools.com/xml/schema_intro.asp
[Tutorials Point]: https://www.tutorialspoint.com/xsd/

282
yasd.py
View File

@ -57,8 +57,8 @@ class YASD:
self.msgr = YASDMessenger(quiet=quiet, log=log) self.msgr = YASDMessenger(quiet=quiet, log=log)
self.yaml = YASDCheck(indata, self.msgr).yaml self.yaml = YASDCheck(indata, self.msgr).yaml
self.formatter = XMLFormatter(indent=2) self.formatter = XMLFormatter(indent=2)
self.soups = self.__get_soups()
self.outfile = outfile self.outfile = outfile
self.xsd = None
self.out = "" self.out = ""
def convert(self, stdout=False): def convert(self, stdout=False):
@ -71,6 +71,7 @@ class YASD:
self.__build_schema() self.__build_schema()
self.__build_elements() self.__build_elements()
self.__build_attributes() self.__build_attributes()
self.__build_groups()
self.__stringify_xsd() self.__stringify_xsd()
if stdout: if stdout:
self.__output() self.__output()
@ -84,8 +85,8 @@ class YASD:
:param stdout: if sample goes to stdout or not; 'False' by default :param stdout: if sample goes to stdout or not; 'False' by default
:type stdout: True or False :type stdout: True or False
""" """
# TODO: XML sample # TODO XML sample
self.out = "TODO: XML sample" self.out = "XML sample"
if stdout: if stdout:
self.__output() self.__output()
else: else:
@ -93,73 +94,69 @@ class YASD:
def document(self, stdout=False): def document(self, stdout=False):
""" """
Generates RST documentation Generates RST documentation.
:param stdout: if document goes to stdout or not; 'False' by default :param stdout: if document goes to stdout or not; 'False' by default
:type stdout: True or False :type stdout: True or False
""" """
# TODO: RST document # TODO RST document
self.out = f"TODO: RST document from :{self.__dict__}" self.out = f"RST document from :{self.__dict__}"
if stdout: if stdout:
self.__output() self.__output()
else: else:
return self.out return self.out
def __build_schema(self): def __build_schema(self):
unwanted = "version schemaLocation".split() """
for key in unwanted: Builds root node for XSD.
self.yaml["schema"].pop(key) """
soup = BeautifulSoup(parser="xml") for key in ["version", "schemaLocation"]:
schema = soup.new_tag("schema", nsprefix="xs") 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" schema["xmlns:xs"] = "http://www.w3.org/2001/XMLSchema"
soup.append(schema)
for key, val in self.yaml["schema"].items(): for key, val in self.yaml["schema"].items():
schema[key] = val schema[key] = val
self.soups["schema"] = soup self.xsd.append(schema)
def __build_elements(self): 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"]: for el in self.yaml["elements"]:
el = self.__sanitize(el) el = self.__sanitize(el)
{ if el["type"] == "simple":
"simple": self.__build_element_simple(soup, el), self.__build_simple(el)
"empty": self.__build_element_empty(soup, el), else:
"no_text": self.__build_element_no_text(soup, el), self.__build_complex(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):
...
def __build_attributes(self): def __build_attributes(self):
soup = BeautifulSoup(parser="xml") """
for el in self.yaml["attributes"]: Builds attributes nodes for XSD.
element = self.__build_simple(self.__sanitize(el), tag="attribute")
soup.append(element) Attributes are always simple types.
self.soups["attributes"] = soup """
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"): def __build_simple(self, el, tag="element"):
soup = BeautifulSoup(parser="xml") """
element = soup.new_tag(tag, nsprefix="xs") Builds simple node for XSD.
soup.append(element)
: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(): for key, val in el.items():
if key == "datatype": if key == "datatype":
element["type"] = f"xs:{val}" element["type"] = f"xs:{val}"
@ -167,18 +164,140 @@ class YASD:
self.__build_restriction(element, val) self.__build_restriction(element, val)
else: else:
element[key] = val element[key] = val
return soup self.xsd.schema.append(element)
def __build_restriction(self, root, elements): def __build_complex(self, el):
soup = BeautifulSoup(parser="xml") """
simple_type = soup.new_tag("simpleType", nsprefix="xs") Builds complex node for XSD.
restriction = soup.new_tag("restriction", nsprefix="xs")
restriction["base"] = self.__get_base(list(elements.keys())[0]) :param dict el: YASD element
for key, val in elements.items(): """
constrain = soup.new_tag(key, nsprefix="xs", value=val) # TODO fix according to 'element' and 'complexType' refs
restriction.append(constrain) # https://www.w3schools.com/xml/schema_elements_ref.asp
simple_type.append(restriction) element = self.__build_complex_root(el)
root.append(simple_type) 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): def __sanitize(self, el):
""" """
@ -194,40 +313,11 @@ class YASD:
del el["description"] del el["description"]
return el 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): def __stringify_xsd(self):
""" """
Converts BeautifulSoups to pretty text format. Converts BeautifulSoups to pretty text format.
""" """
xsd = self.soups["schema"] self.out = self.xsd.prettify(formatter=self.formatter)
del self.soups["schema"]
for key, val in self.soups.items():
xsd.append(val)
self.out = xsd.prettify(formatter=self.formatter)
def __output(self, extname=".xsd"): def __output(self, extname=".xsd"):
""" """
@ -301,7 +391,7 @@ class YASDCheck:
:return: YASD structure :return: YASD structure
:rtype: dict :rtype: dict
""" """
# TODO: extra checks for self.yaml # TODO extra checks for self.yaml
... ...
@ -313,10 +403,8 @@ class YASDMessenger:
def keys(): def keys():
""" """
Messages keys dictionary. Messages keys dictionary.
Here multilang support could be implemented with:
https://github.com/sectasy0/pyi18n
""" """
# TODO internationalization with: https://github.com/sectasy0/pyi18n
return { return {
"description": """ "description": """
YASD, Yet Another Schema Definition. YASD is a YAML format for YASD, Yet Another Schema Definition. YASD is a YAML format for
@ -363,7 +451,7 @@ class YASDMessenger:
self.__check_level(level) self.__check_level(level)
msg = self.__get_msg(key, **kwargs) msg = self.__get_msg(key, **kwargs)
msg = f"[{level.upper()}] {msg}" 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) print(msg)
if level in ["error", "fatal"]: if level in ["error", "fatal"]:
sys.exit(1) sys.exit(1)
@ -410,8 +498,8 @@ class YASDCLI:
self.__init_parser() self.__init_parser()
args = self.parser.parse_args() args = self.parser.parse_args()
if args.action == "man": if args.action == "man":
# TODO: print man from README # TODO print man from README
print("TODO: MAN") print("Manual")
else: else:
YASD.do(args.action, args.input, args.output, args.quiet, args.log) YASD.do(args.action, args.input, args.output, args.quiet, args.log)