mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-07-27 23:21:02 +03:00
963 lines
39 KiB
Python
963 lines
39 KiB
Python
# xdto-compile v1.1 — Build a 1C XDTO package from an XML Schema (XSD) (Python port)
|
|
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import uuid
|
|
|
|
from lxml import etree
|
|
|
|
# Эти пространства имён предоставляет сама платформа — пакетов в конфигурации
|
|
# для них нет и быть не должно (выведено по корпусу)
|
|
PLATFORM_NS = {
|
|
"http://v8.1c.ru/8.1/data/core",
|
|
"http://v8.1c.ru/8.1/data/enterprise",
|
|
"http://v8.1c.ru/8.1/data/enterprise/current-config",
|
|
"http://v8.1c.ru/8.1/data-composition-system/settings",
|
|
"http://v8.1c.ru/8.3/data/ext",
|
|
"http://www.w3.org/2001/XMLSchema",
|
|
}
|
|
|
|
|
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
sys.stderr.reconfigure(encoding="utf-8")
|
|
|
|
XDTO_NS = "http://v8.1c.ru/8.1/xdto"
|
|
XS_NS = "http://www.w3.org/2001/XMLSchema"
|
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
|
MD_NS = "http://v8.1c.ru/8.3/MDClasses"
|
|
|
|
parser = argparse.ArgumentParser(allow_abbrev=False)
|
|
parser.add_argument("-XsdPath", "-Path", default="")
|
|
parser.add_argument("-Xsd", default="")
|
|
parser.add_argument("-OutputDir", required=True)
|
|
parser.add_argument("-Name", default="")
|
|
parser.add_argument("-Synonym", default="")
|
|
parser.add_argument("-Comment", default="")
|
|
parser.add_argument("-Force", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
|
|
def _parse_xml(source, from_string=False):
|
|
"""Разбор с узким отступлением для не-URI пространств имён.
|
|
|
|
Платформа допускает в targetNamespace произвольную строку (в выгрузке БП есть
|
|
пакет с кириллическим «ДопФайлУниверсальный»), .NET такое принимает, а libxml2
|
|
отвергает. Откатываемся на восстанавливающий разбор ТОЛЬКО на этой ошибке,
|
|
иначе по-настоящему битый XML перестал бы отличаться от корректного.
|
|
"""
|
|
try:
|
|
return (etree.fromstring(source) if from_string else etree.parse(source))
|
|
except etree.XMLSyntaxError as e:
|
|
if "is not a valid URI" not in str(e):
|
|
raise
|
|
p = etree.XMLParser(recover=True)
|
|
return (etree.fromstring(source, p) if from_string else etree.parse(source, p))
|
|
|
|
# ── support guard (Ext/ParentConfigurations.bin) ─────────────
|
|
# См. docs/1c-support-state-spec.md. Блокирует правку объектов поставщика
|
|
# «на замке». Триггер — наличие bin; реакция из .v8-project.json
|
|
# editingAllowedCheck (deny|warn|off, по умолчанию deny).
|
|
|
|
|
|
def find_v8_project(start_dir):
|
|
d = os.path.abspath(start_dir)
|
|
for _ in range(20):
|
|
pj = os.path.join(d, ".v8-project.json")
|
|
if os.path.exists(pj):
|
|
return pj
|
|
parent = os.path.dirname(d)
|
|
if parent == d:
|
|
break
|
|
d = parent
|
|
return None
|
|
|
|
|
|
def get_edit_mode(cfg_dir):
|
|
try:
|
|
pj = find_v8_project(cfg_dir)
|
|
if pj:
|
|
with open(pj, encoding="utf-8-sig") as f:
|
|
cfg = json.load(f)
|
|
return str(cfg.get("editingAllowedCheck") or "deny")
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
return "deny"
|
|
|
|
|
|
def is_external_object_root(xml_path):
|
|
try:
|
|
root = _parse_xml(xml_path).getroot()
|
|
for el in root:
|
|
if isinstance(el.tag, str):
|
|
return etree.QName(el).localname in ("ExternalDataProcessor", "ExternalReport")
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
return False
|
|
|
|
|
|
def assert_edit_allowed(target_path):
|
|
d = os.path.abspath(target_path)
|
|
for _ in range(20):
|
|
# Автономный объект (внешняя обработка/отчёт) — граница климба
|
|
try:
|
|
for f in os.listdir(d):
|
|
if f.endswith(".xml") and is_external_object_root(os.path.join(d, f)):
|
|
return
|
|
except OSError:
|
|
pass
|
|
cfg_xml = os.path.join(d, "Configuration.xml")
|
|
support_bin = os.path.join(d, "Ext", "ParentConfigurations.bin")
|
|
if os.path.exists(cfg_xml):
|
|
if os.path.exists(support_bin):
|
|
mode = get_edit_mode(d)
|
|
if mode == "off":
|
|
return
|
|
msg = ("Конфигурация находится на поддержке (Ext/ParentConfigurations.bin). "
|
|
"Правка может быть запрещена.")
|
|
if mode == "warn":
|
|
print(f"WARNING: {msg}", file=sys.stderr)
|
|
return
|
|
print(f"{msg} Снимите с поддержки (/support-edit) или задайте "
|
|
"editingAllowedCheck в .v8-project.json.", file=sys.stderr)
|
|
sys.exit(1)
|
|
return
|
|
parent = os.path.dirname(d)
|
|
if parent == d:
|
|
break
|
|
d = parent
|
|
|
|
|
|
# ── load the schema ──────────────────────────────────────────
|
|
|
|
if args.Xsd:
|
|
xsd_bytes = args.Xsd.encode("utf-8")
|
|
default_name = "Package"
|
|
elif args.XsdPath:
|
|
if not os.path.isfile(args.XsdPath):
|
|
print(f"Файл XSD не найден: {args.XsdPath}", file=sys.stderr)
|
|
sys.exit(1)
|
|
with open(args.XsdPath, "rb") as f:
|
|
xsd_bytes = f.read()
|
|
default_name = os.path.splitext(os.path.basename(args.XsdPath))[0]
|
|
else:
|
|
print("Укажите -XsdPath или -Xsd", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
try:
|
|
schema = _parse_xml(xsd_bytes, from_string=True)
|
|
except Exception as e: # noqa: BLE001
|
|
print(f"Не удалось разобрать XSD: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def local(el):
|
|
return etree.QName(el).localname
|
|
|
|
|
|
if local(schema) != "schema" or etree.QName(schema).namespace != XS_NS:
|
|
print(f"Ожидался корневой <xs:schema> в пространстве имён {XS_NS}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
target_ns = schema.get("targetNamespace") or ""
|
|
|
|
# ── emit-tree primitives ─────────────────────────────────────
|
|
|
|
|
|
class Node:
|
|
__slots__ = ("tag", "attrs", "children", "text", "prefix", "declare_ns")
|
|
|
|
def __init__(self, tag):
|
|
self.tag = tag
|
|
self.attrs = [] # список dict: name, value | (ns, local) | list
|
|
self.children = []
|
|
self.text = None
|
|
self.prefix = None
|
|
self.declare_ns = None
|
|
|
|
|
|
def add_attr(node, name, value):
|
|
if value is None:
|
|
return
|
|
node.attrs.append({"name": name, "value": str(value)})
|
|
|
|
|
|
def add_qattr(node, name, ns, loc):
|
|
if loc is None:
|
|
return
|
|
node.attrs.append({"name": name, "ns": ns, "local": loc})
|
|
|
|
|
|
def add_qlist_attr(node, name, pairs, clark):
|
|
if not pairs:
|
|
return
|
|
node.attrs.append({"name": name, "list": pairs, "clark": clark})
|
|
|
|
|
|
# Канонический порядок атрибутов — топологическая сортировка по корпусу 8.3.24
|
|
# (acc + erp, 760 пакетов), см. docs/1c-xdto-spec.md.
|
|
ATTR_ORDER = {
|
|
"package": ["targetNamespace", "elementFormQualified", "attributeFormQualified"],
|
|
"import": ["namespace"],
|
|
"objectType": ["name", "base", "open", "abstract", "mixed", "ordered", "sequenced"],
|
|
"property": ["name", "ref", "type", "lowerBound", "upperBound", "nillable",
|
|
"fixed", "default", "form", "localName", "qualified"],
|
|
"valueType": ["name", "base", "variety", "itemType", "length", "memberTypes",
|
|
"minExclusive", "maxExclusive", "minInclusive", "maxInclusive",
|
|
"minLength", "maxLength", "totalDigits", "fractionDigits", "whiteSpace"],
|
|
"typeDef": ["xsi:type", "base", "mixed", "open", "ordered", "sequenced", "variety",
|
|
"itemType", "length", "memberTypes", "minExclusive", "maxExclusive",
|
|
"minInclusive", "maxInclusive", "minLength", "maxLength",
|
|
"totalDigits", "fractionDigits", "whiteSpace"],
|
|
"enumeration": ["xsi:type"],
|
|
}
|
|
|
|
|
|
def sort_attrs(node):
|
|
order = ATTR_ORDER.get(node.tag)
|
|
if not order:
|
|
return node.attrs
|
|
res = []
|
|
for n in order:
|
|
res.extend(a for a in node.attrs if a["name"] == n)
|
|
res.extend(a for a in node.attrs if a["name"] not in order)
|
|
return res
|
|
|
|
|
|
def esc(s):
|
|
if s is None:
|
|
return ""
|
|
return str(s).replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """)
|
|
|
|
|
|
def esc_text(s):
|
|
if s is None:
|
|
return ""
|
|
return str(s).replace("&", "&").replace("<", "<").replace(">", ">")
|
|
|
|
|
|
# ── serializer with the dNpM prefix scheme ───────────────────
|
|
|
|
out = []
|
|
|
|
|
|
def serialize_node(node, depth, inherited):
|
|
indent = "\t" * (depth - 1)
|
|
attrs_sorted = sort_attrs(node)
|
|
|
|
# Объявляем здесь только те ns, которых ещё нет в области видимости:
|
|
# сериализатор платформы объявляет префикс на первом нуждающемся узле,
|
|
# а потомки переиспользуют — отсюда d2p1 у property внутри objectType.
|
|
local_ns = []
|
|
|
|
def need_prefix(ns):
|
|
if not ns or ns in (XS_NS, XSI_NS):
|
|
return
|
|
if ns in inherited:
|
|
return
|
|
if ns not in local_ns:
|
|
local_ns.append(ns)
|
|
|
|
for a in attrs_sorted:
|
|
if "list" in a:
|
|
# Нотация Кларка несёт ns в значении и префикса не требует
|
|
if not a["clark"]:
|
|
for p in a["list"]:
|
|
need_prefix(p[0])
|
|
elif a.get("ns"):
|
|
need_prefix(a["ns"])
|
|
|
|
has_qualified = any(a["name"] == "qualified" for a in attrs_sorted)
|
|
if has_qualified:
|
|
need_prefix(XDTO_NS)
|
|
if node.declare_ns:
|
|
need_prefix(node.declare_ns)
|
|
|
|
prefix_of = dict(inherited)
|
|
ns_decls = ""
|
|
for i, ns in enumerate(local_ns):
|
|
# Осмысленный префикс из исходника (зеркало xdto:prefix) имеет приоритет
|
|
px = node.prefix if (i == 0 and node.prefix) else f"d{depth}p{i + 1}"
|
|
prefix_of[ns] = px
|
|
ns_decls += f' xmlns:{px}="{esc(ns)}"'
|
|
|
|
def qval(ns, loc):
|
|
if not ns:
|
|
return loc
|
|
if ns == XS_NS:
|
|
return f"xs:{loc}"
|
|
if ns == XSI_NS:
|
|
return f"xsi:{loc}"
|
|
return f"{prefix_of[ns]}:{loc}"
|
|
|
|
attr_text = ""
|
|
for a in attrs_sorted:
|
|
if "list" in a:
|
|
vals = []
|
|
for ns, loc in a["list"]:
|
|
vals.append((f"{{{ns}}}{loc}" if ns else loc) if a["clark"] else qval(ns, loc))
|
|
attr_text += f' {a["name"]}="{esc(" ".join(vals))}"'
|
|
elif a.get("ns") or a.get("local"):
|
|
attr_text += f' {a["name"]}="{esc(qval(a.get("ns"), a["local"]))}"'
|
|
elif a["name"] == "qualified":
|
|
attr_text += f' {prefix_of[XDTO_NS]}:qualified="{esc(a["value"])}"'
|
|
else:
|
|
attr_text += f' {a["name"]}="{esc(a["value"])}"'
|
|
|
|
tag_name = f"{prefix_of[XDTO_NS]}:{node.tag}" if has_qualified else node.tag
|
|
|
|
has_children = bool(node.children)
|
|
# Пустое значение пишется самозакрывающимся тегом: <enumeration/>
|
|
has_text = node.text is not None and node.text != ""
|
|
|
|
if not has_children and not has_text:
|
|
out.append(f"{indent}<{tag_name}{ns_decls}{attr_text}/>\r\n")
|
|
return
|
|
if has_text and not has_children:
|
|
out.append(f"{indent}<{tag_name}{ns_decls}{attr_text}>{esc_text(node.text)}</{tag_name}>\r\n")
|
|
return
|
|
out.append(f"{indent}<{tag_name}{ns_decls}{attr_text}>\r\n")
|
|
for c in node.children:
|
|
serialize_node(c, depth + 1, prefix_of)
|
|
out.append(f"{indent}</{tag_name}>\r\n")
|
|
|
|
|
|
# ── XSD reading helpers ──────────────────────────────────────
|
|
|
|
# Предупреждения о том, что XSD выражает, а модель XDTO — нет. Молча ронять
|
|
# такие конструкции нельзя: пакет соберётся, а половина свойств исчезнет.
|
|
warnings_list = []
|
|
|
|
|
|
def warn(msg):
|
|
if msg not in warnings_list:
|
|
warnings_list.append(msg)
|
|
|
|
|
|
GROUPS = {}
|
|
ATTR_GROUPS = {}
|
|
|
|
|
|
def MA(el, name):
|
|
# xdto: mirror attribute — литеральное значение для Package.bin.
|
|
# Ищем по namespace, а не по строке префикса.
|
|
return el.get(f"{{{XDTO_NS}}}{name}")
|
|
|
|
|
|
def xchildren(el, name):
|
|
return [c for c in el if isinstance(c.tag, str)
|
|
and etree.QName(c).namespace == XS_NS and local(c) == name]
|
|
|
|
|
|
def xfirst(el, name):
|
|
r = xchildren(el, name)
|
|
return r[0] if r else None
|
|
|
|
|
|
def split_qname(el, qname):
|
|
if not qname:
|
|
return None
|
|
parts = qname.split(":")
|
|
if len(parts) == 2:
|
|
ns = el.nsmap.get(parts[0])
|
|
loc = parts[1]
|
|
else:
|
|
# Прощающий ввод: голое имя типа — тип целевого пространства имён
|
|
ns = el.nsmap.get(None) or target_ns
|
|
loc = parts[0]
|
|
return (ns, loc)
|
|
|
|
|
|
def split_qname_list(el, lst):
|
|
if not lst:
|
|
return []
|
|
return [split_qname(el, q) for q in lst.split() if q]
|
|
|
|
|
|
FACETS = ["length", "minLength", "maxLength", "totalDigits", "fractionDigits",
|
|
"minInclusive", "maxInclusive", "minExclusive", "maxExclusive", "whiteSpace"]
|
|
|
|
|
|
# ── simpleType -> valueType / typeDef(ValueType) ─────────────
|
|
|
|
def fill_simple_type(node, st):
|
|
restriction = xfirst(st, "restriction")
|
|
lst = xfirst(st, "list")
|
|
union = xfirst(st, "union")
|
|
|
|
if lst is not None:
|
|
it = split_qname(lst, lst.get("itemType"))
|
|
mv = MA(lst, "variety")
|
|
add_attr(node, "variety", mv if mv is not None else "List")
|
|
if it:
|
|
add_qattr(node, "itemType", it[0], it[1])
|
|
return
|
|
if union is not None:
|
|
mv = MA(union, "variety")
|
|
set_attr_value(node, "variety", mv if mv is not None else "Union")
|
|
members = split_qname_list(union, union.get("memberTypes"))
|
|
# По умолчанию нотация Кларка — так записано 125 из 135 memberTypes корпуса
|
|
use_clark = MA(union, "memberTypesForm") != "prefixed"
|
|
if members:
|
|
add_qlist_attr(node, "memberTypes", members, use_clark)
|
|
node.declare_ns = MA(union, "declareNs")
|
|
for anon in xchildren(union, "simpleType"):
|
|
# typeDef в контексте простого типа xsi:type не несёт (40 узлов корпуса)
|
|
td = Node("typeDef")
|
|
fill_simple_type(td, anon)
|
|
node.children.append(td)
|
|
return
|
|
if restriction is not None:
|
|
b = split_qname(restriction, restriction.get("base"))
|
|
if b:
|
|
add_qattr(node, "base", b[0], b[1])
|
|
mv = MA(restriction, "variety")
|
|
if mv is not None:
|
|
add_attr(node, "variety", mv)
|
|
# Анонимный базовый тип внутри xs:restriction — typeDef без xsi:type
|
|
anon_base = xfirst(restriction, "simpleType")
|
|
if anon_base is not None:
|
|
td = Node("typeDef")
|
|
fill_simple_type(td, anon_base)
|
|
node.children.append(td)
|
|
for f in FACETS:
|
|
for fe in xchildren(restriction, f):
|
|
add_attr(node, f, fe.get("value"))
|
|
for pe in xchildren(restriction, "pattern"):
|
|
pn = Node("pattern")
|
|
pn.text = pe.get("value")
|
|
node.children.append(pn)
|
|
for en in xchildren(restriction, "enumeration"):
|
|
enode = Node("enumeration")
|
|
mt = MA(en, "type")
|
|
if mt is not None:
|
|
q = split_qname(en, mt)
|
|
add_qattr(enode, "xsi:type", q[0], q[1])
|
|
enode.text = en.get("value")
|
|
node.children.append(enode)
|
|
|
|
|
|
def set_attr_value(node, name, value):
|
|
for a in node.attrs:
|
|
if a["name"] == name:
|
|
a["value"] = value
|
|
return
|
|
add_attr(node, name, value)
|
|
|
|
|
|
def get_prop_key(p):
|
|
for a in p.attrs:
|
|
if a["name"] == "name":
|
|
return a.get("value")
|
|
for a in p.attrs:
|
|
if a["name"] == "ref":
|
|
return "@" + a["local"]
|
|
return None
|
|
|
|
|
|
def reorder_properties(node, names):
|
|
props = [c for c in node.children if c.tag == "property"]
|
|
if len(props) < 2:
|
|
return
|
|
by_key = {}
|
|
for p in props:
|
|
k = get_prop_key(p)
|
|
if k is not None and k not in by_key:
|
|
by_key[k] = p
|
|
ordered = []
|
|
for n in names:
|
|
if n in by_key:
|
|
ordered.append(by_key.pop(n))
|
|
for p in props:
|
|
if p not in ordered:
|
|
ordered.append(p)
|
|
others = [c for c in node.children if c.tag != "property"]
|
|
node.children = ordered + others
|
|
|
|
|
|
# ── element / attribute -> property ──────────────────────────
|
|
|
|
def build_property(el, is_attribute):
|
|
p = Node("property")
|
|
|
|
xsd_name = el.get("name")
|
|
mirror_name = MA(el, "name")
|
|
if mirror_name is not None:
|
|
add_attr(p, "name", mirror_name)
|
|
local_name = xsd_name
|
|
else:
|
|
add_attr(p, "name", xsd_name)
|
|
local_name = None
|
|
|
|
ref_q = split_qname(el, el.get("ref"))
|
|
if ref_q:
|
|
add_qattr(p, "ref", ref_q[0], ref_q[1])
|
|
|
|
type_q = split_qname(el, el.get("type"))
|
|
if type_q:
|
|
add_qattr(p, "type", type_q[0], type_q[1])
|
|
|
|
if is_attribute:
|
|
add_attr(p, "lowerBound", MA(el, "lowerBound"))
|
|
add_attr(p, "upperBound", MA(el, "upperBound"))
|
|
add_attr(p, "nillable", MA(el, "nillable"))
|
|
else:
|
|
add_attr(p, "lowerBound", el.get("minOccurs"))
|
|
max_occ = el.get("maxOccurs")
|
|
if max_occ is not None:
|
|
add_attr(p, "upperBound", "-1" if max_occ == "unbounded" else max_occ)
|
|
add_attr(p, "nillable", el.get("nillable"))
|
|
|
|
# XSD-шный fixed="V" несёт значение, в модели это fixed="true" + default="V".
|
|
# Прощающий ввод: модельная форма через зеркало xdto:fixed тоже принимается.
|
|
m_fixed = MA(el, "fixed")
|
|
if m_fixed is not None:
|
|
add_attr(p, "fixed", m_fixed)
|
|
add_attr(p, "default", el.get("default"))
|
|
if m_fixed == "true" and el.get("default") is None:
|
|
warn('Свойство "' + str(el.get("name")) + '": xdto:fixed="true" без default — '
|
|
"платформа отвергнет пакет («Отсутствует фиксированное значение»). "
|
|
'Значение задаётся атрибутом default, либо пишите XSD-форму fixed="значение"')
|
|
elif el.get("fixed") is not None:
|
|
add_attr(p, "fixed", "true")
|
|
add_attr(p, "default", el.get("fixed"))
|
|
else:
|
|
add_attr(p, "default", el.get("default"))
|
|
|
|
if is_attribute:
|
|
add_attr(p, "form", "Attribute")
|
|
else:
|
|
mf = MA(el, "form")
|
|
if mf is not None:
|
|
add_attr(p, "form", mf)
|
|
add_attr(p, "localName", local_name)
|
|
add_attr(p, "qualified", MA(el, "qualified"))
|
|
p.prefix = MA(el, "prefix")
|
|
|
|
anon_simple = xfirst(el, "simpleType")
|
|
anon_complex = xfirst(el, "complexType")
|
|
if anon_simple is not None:
|
|
td = Node("typeDef")
|
|
add_attr(td, "xsi:type", "ValueType")
|
|
fill_simple_type(td, anon_simple)
|
|
p.children.append(td)
|
|
elif anon_complex is not None:
|
|
td = Node("typeDef")
|
|
add_attr(td, "xsi:type", "ObjectType")
|
|
fill_complex_type(td, anon_complex)
|
|
p.children.append(td)
|
|
return p
|
|
|
|
|
|
# ── complexType -> objectType / typeDef(ObjectType) ──────────
|
|
|
|
def resolve_group(el, kind):
|
|
ref = el.get("ref")
|
|
if not ref:
|
|
return None
|
|
q = split_qname(el, ref)
|
|
if not q:
|
|
return None
|
|
m = GROUPS if kind == "group" else ATTR_GROUPS
|
|
return m.get(q[1])
|
|
|
|
|
|
# Модель XDTO знает только плоский список свойств: вложенные частицы уплощаются.
|
|
# Каждое уплощение — предупреждение, потому что меняется смысл схемы.
|
|
def collect_particle(particle, elem_list, open_flag, type_name, depth, optionalize=False):
|
|
if depth > 20:
|
|
return
|
|
for c in particle:
|
|
if not isinstance(c.tag, str) or etree.QName(c).namespace != XS_NS:
|
|
continue
|
|
ln = local(c)
|
|
if ln == "element":
|
|
prop = build_property(c, False)
|
|
# Ветка уплощённого xs:choice обязана стать необязательной: иначе
|
|
# «одно из двух» превращается в «оба сразу», и тип нельзя заполнить
|
|
if optionalize:
|
|
set_attr_value(prop, "lowerBound", "0")
|
|
elem_list.append(prop)
|
|
elif ln == "any":
|
|
open_flag[0] = True
|
|
elif ln == "sequence":
|
|
warn(type_name + " : вложенная xs:sequence уплощена — модель XDTO хранит плоский список свойств")
|
|
collect_particle(c, elem_list, open_flag, type_name, depth + 1, optionalize)
|
|
elif ln == "choice":
|
|
branches = [b.get("name") for b in c
|
|
if isinstance(b.tag, str) and etree.QName(b).namespace == XS_NS and b.get("name")]
|
|
lst = (" (" + ", ".join(branches) + ")") if branches else ""
|
|
warn(type_name + " : вложенная xs:choice уплощена — ветки" + lst + " сделаны необязательными. "
|
|
"Выбор одного из вариантов не сохранён: модель не запретит заполнить "
|
|
"сразу несколько или ни одного")
|
|
collect_particle(c, elem_list, open_flag, type_name, depth + 1, True)
|
|
elif ln == "all":
|
|
warn(type_name + " : xs:all трактуется как последовательность")
|
|
collect_particle(c, elem_list, open_flag, type_name, depth + 1, optionalize)
|
|
elif ln == "group":
|
|
g = resolve_group(c, "group")
|
|
if g is not None:
|
|
for gc in g:
|
|
if isinstance(gc.tag, str) and etree.QName(gc).namespace == XS_NS \
|
|
and local(gc) in ("sequence", "choice", "all"):
|
|
collect_particle(gc, elem_list, open_flag, type_name, depth + 1, optionalize)
|
|
else:
|
|
warn(type_name + " : не найдена группа " + str(c.get("ref")) + " — её свойства в пакет не попали")
|
|
if ln in ("sequence", "choice", "all", "group"):
|
|
if c.get("maxOccurs") is not None or c.get("minOccurs") is not None:
|
|
warn(type_name + " : кратность на вложенной частице (<xs:" + ln +
|
|
" minOccurs/maxOccurs>) не выражается в модели XDTO")
|
|
|
|
|
|
def set_type_flags(node, ct, is_open, choice):
|
|
m_open = MA(ct, "open")
|
|
if m_open is not None:
|
|
add_attr(node, "open", m_open)
|
|
elif is_open:
|
|
add_attr(node, "open", "true")
|
|
|
|
m_ordered = MA(ct, "ordered")
|
|
if m_ordered is not None:
|
|
add_attr(node, "ordered", m_ordered)
|
|
elif choice is not None:
|
|
add_attr(node, "ordered", "false")
|
|
|
|
m_seq = MA(ct, "sequenced")
|
|
if m_seq is not None:
|
|
add_attr(node, "sequenced", m_seq)
|
|
|
|
m_abstract = MA(ct, "abstract")
|
|
if m_abstract is not None:
|
|
add_attr(node, "abstract", m_abstract)
|
|
elif ct.get("abstract") == "true":
|
|
add_attr(node, "abstract", "true")
|
|
|
|
m_mixed = MA(ct, "mixed")
|
|
if m_mixed is not None:
|
|
add_attr(node, "mixed", m_mixed)
|
|
elif ct.get("mixed") == "true":
|
|
add_attr(node, "mixed", "true")
|
|
|
|
|
|
def fill_complex_type(node, ct):
|
|
body = ct
|
|
content = xfirst(ct, "complexContent")
|
|
if content is not None:
|
|
ext = xfirst(content, "extension")
|
|
if ext is not None:
|
|
b = split_qname(ext, ext.get("base"))
|
|
if b:
|
|
add_qattr(node, "base", b[0], b[1])
|
|
body = ext
|
|
|
|
# xs:simpleContent -> свойство "Text", хранящее значение самого элемента
|
|
simple = xfirst(ct, "simpleContent")
|
|
if simple is not None:
|
|
ext = xfirst(simple, "extension")
|
|
if ext is not None:
|
|
for a in xchildren(ext, "attribute"):
|
|
node.children.append(build_property(a, True))
|
|
tp = Node("property")
|
|
t_name = MA(ext, "textName")
|
|
add_attr(tp, "name", t_name if t_name is not None else "__content")
|
|
b = split_qname(ext, ext.get("base"))
|
|
if b:
|
|
add_qattr(tp, "type", b[0], b[1])
|
|
add_attr(tp, "lowerBound", MA(ext, "textlowerBound"))
|
|
add_attr(tp, "upperBound", MA(ext, "textupperBound"))
|
|
add_attr(tp, "nillable", MA(ext, "textnillable"))
|
|
add_attr(tp, "form", "Text")
|
|
node.children.append(tp)
|
|
# xs:simpleContent не отменяет флаги самого xs:complexType
|
|
set_type_flags(node, ct, False, None)
|
|
return
|
|
|
|
seq = xfirst(body, "sequence")
|
|
cho = xfirst(body, "choice")
|
|
all_ = xfirst(body, "all")
|
|
grp = xfirst(body, "group")
|
|
particle = seq if seq is not None else (cho if cho is not None else (all_ if all_ is not None else grp))
|
|
open_flag = [False]
|
|
|
|
# Порядок в XDTO: сначала form="Attribute", потом остальные (96.5% типов корпуса)
|
|
elem_props = []
|
|
type_name = ct.get("name") or "(анонимный тип)"
|
|
if particle is not None:
|
|
if all_ is not None:
|
|
warn(type_name + " : xs:all трактуется как последовательность")
|
|
if grp is not None and seq is None and cho is None and all_ is None:
|
|
# Корневая частица задана ссылкой на группу — раскрываем её содержимое
|
|
g = resolve_group(grp, "group")
|
|
if g is not None:
|
|
for gc in g:
|
|
if isinstance(gc.tag, str) and etree.QName(gc).namespace == XS_NS \
|
|
and local(gc) in ("sequence", "choice", "all"):
|
|
collect_particle(gc, elem_props, open_flag, type_name, 1)
|
|
else:
|
|
warn(type_name + " : не найдена группа " + str(grp.get("ref")) + " — её свойства в пакет не попали")
|
|
else:
|
|
collect_particle(particle, elem_props, open_flag, type_name, 0)
|
|
is_open = open_flag[0]
|
|
for a in xchildren(body, "attribute"):
|
|
node.children.append(build_property(a, True))
|
|
# xs:attributeGroup раскрываем по ссылке
|
|
for ag in xchildren(body, "attributeGroup"):
|
|
g = resolve_group(ag, "attributeGroup")
|
|
if g is not None:
|
|
for a in xchildren(g, "attribute"):
|
|
node.children.append(build_property(a, True))
|
|
else:
|
|
warn("Не найдена группа атрибутов " + str(ag.get("ref")) + " — её атрибуты в пакет не попали")
|
|
node.children.extend(elem_props)
|
|
if xchildren(body, "anyAttribute"):
|
|
is_open = True
|
|
|
|
m_order = MA(ct, "order")
|
|
if m_order is not None:
|
|
reorder_properties(node, m_order.split("|"))
|
|
|
|
set_type_flags(node, ct, is_open, cho)
|
|
|
|
|
|
# ── build the package tree ───────────────────────────────────
|
|
|
|
pkg_node = Node("package")
|
|
add_attr(pkg_node, "targetNamespace", target_ns)
|
|
|
|
efq_mirror = MA(schema, "elementFormQualified")
|
|
afq_mirror = MA(schema, "attributeFormQualified")
|
|
efd = schema.get("elementFormDefault")
|
|
afd = schema.get("attributeFormDefault")
|
|
if efq_mirror is not None:
|
|
add_attr(pkg_node, "elementFormQualified", efq_mirror)
|
|
elif efd is not None:
|
|
add_attr(pkg_node, "elementFormQualified", "true" if efd == "qualified" else "false")
|
|
if afq_mirror is not None:
|
|
add_attr(pkg_node, "attributeFormQualified", afq_mirror)
|
|
elif afd is not None:
|
|
add_attr(pkg_node, "attributeFormQualified", "true" if afd == "qualified" else "false")
|
|
|
|
meta_name = meta_comment = None
|
|
meta_synonym = []
|
|
ann = xfirst(schema, "annotation")
|
|
if ann is not None:
|
|
appinfo = xfirst(ann, "appinfo")
|
|
if appinfo is not None:
|
|
for pk in appinfo:
|
|
if not isinstance(pk.tag, str) or etree.QName(pk).namespace != XDTO_NS:
|
|
continue
|
|
for f in pk:
|
|
if not isinstance(f.tag, str):
|
|
continue
|
|
ln = local(f)
|
|
if ln == "name":
|
|
meta_name = f.text or ""
|
|
elif ln == "comment":
|
|
meta_comment = f.text or ""
|
|
elif ln == "synonym":
|
|
meta_synonym.append({"Lang": f.get("lang") or "", "Content": f.text or ""})
|
|
|
|
# Реестр глобальных групп — нужен до обхода, чтобы раскрывать ссылки
|
|
for node in schema:
|
|
if not isinstance(node.tag, str) or etree.QName(node).namespace != XS_NS:
|
|
continue
|
|
nm = node.get("name")
|
|
if local(node) == "group" and nm:
|
|
GROUPS[nm] = node
|
|
if local(node) == "attributeGroup" and nm:
|
|
ATTR_GROUPS[nm] = node
|
|
|
|
# Конструкции XSD, которым в модели XDTO нет соответствия
|
|
for sg in schema.iter():
|
|
if isinstance(sg.tag, str) and local(sg) == "element" and sg.get("substitutionGroup"):
|
|
warn("Подстановочные группы (substitutionGroup) не поддерживаются моделью XDTO — объявление "
|
|
+ str(sg.get("name")) + " сохранено как обычное")
|
|
for idc in ("key", "keyref", "unique"):
|
|
if any(isinstance(e.tag, str) and local(e) == idc for e in schema.iter()):
|
|
warn("Ограничения целостности (xs:" + idc + ") в модели XDTO не хранятся — отброшены")
|
|
if any(isinstance(e.tag, str) and local(e) == "redefine" for e in schema.iter()):
|
|
warn("xs:redefine не поддерживается — переопределения проигнорированы")
|
|
if any(isinstance(e.tag, str) and local(e) == "include" for e in schema.iter()):
|
|
warn("xs:include проигнорирован: модель XDTO разрешает зависимости только по namespace. "
|
|
"Соберите включаемую схему отдельным пакетом и добавьте <xs:import>")
|
|
|
|
for node in schema:
|
|
if not isinstance(node.tag, str) or etree.QName(node).namespace != XS_NS:
|
|
continue
|
|
ln = local(node)
|
|
if ln == "import":
|
|
n = Node("import")
|
|
add_attr(n, "namespace", node.get("namespace"))
|
|
pkg_node.children.append(n)
|
|
elif ln in ("annotation", "include", "group", "attributeGroup", "notation"):
|
|
continue
|
|
elif ln == "element":
|
|
pkg_node.children.append(build_property(node, False))
|
|
elif ln == "attribute":
|
|
pkg_node.children.append(build_property(node, True))
|
|
elif ln == "simpleType":
|
|
n = Node("valueType")
|
|
add_attr(n, "name", node.get("name"))
|
|
fill_simple_type(n, node)
|
|
pkg_node.children.append(n)
|
|
elif ln == "complexType":
|
|
n = Node("objectType")
|
|
add_attr(n, "name", node.get("name"))
|
|
fill_complex_type(n, node)
|
|
pkg_node.children.append(n)
|
|
|
|
# ── serialize Package.bin ────────────────────────────────────
|
|
|
|
# Модель XDTO требует строгой последовательности элементов верхнего уровня:
|
|
# import → property → valueType → objectType. Порядок объявлений в XSD произвольный,
|
|
# поэтому пересортировываем — иначе платформа отвергает пакет с «Ошибка преобразования
|
|
# данных XDTO». Все 760 пакетов корпуса этому порядку удовлетворяют.
|
|
TOP_ORDER = ["import", "property", "valueType", "objectType"]
|
|
pkg_node.children = (
|
|
[c for t in TOP_ORDER for c in pkg_node.children if c.tag == t]
|
|
+ [c for c in pkg_node.children if c.tag not in TOP_ORDER]
|
|
)
|
|
|
|
root_attr_text = "".join(f' {a["name"]}="{esc(a["value"])}"' for a in sort_attrs(pkg_node))
|
|
out.append(f'<package xmlns="{XDTO_NS}" xmlns:xs="{XS_NS}" xmlns:xsi="{XSI_NS}"{root_attr_text}>\r\n')
|
|
for c in pkg_node.children:
|
|
serialize_node(c, 2, {})
|
|
out.append("</package>")
|
|
|
|
bin_text = "".join(out)
|
|
|
|
# ── resolve the package name ─────────────────────────────────
|
|
|
|
name = args.Name or meta_name or default_name
|
|
name = re.sub(r"[^\wЀ-ӿ]", "_", name, flags=re.UNICODE)
|
|
if re.match(r"^\d", name):
|
|
name = "_" + name
|
|
|
|
assert_edit_allowed(args.OutputDir)
|
|
|
|
pkg_root = os.path.join(args.OutputDir, "XDTOPackages")
|
|
pkg_dir = os.path.join(pkg_root, name)
|
|
ext_dir = os.path.join(pkg_dir, "Ext")
|
|
md_file = os.path.join(pkg_root, name + ".xml")
|
|
bin_file = os.path.join(ext_dir, "Package.bin")
|
|
|
|
if os.path.exists(bin_file) and not args.Force:
|
|
print(f"Пакет уже существует: {bin_file}. Используйте -Force для перезаписи.", file=sys.stderr)
|
|
sys.exit(1)
|
|
os.makedirs(ext_dir, exist_ok=True)
|
|
|
|
with open(bin_file, "wb") as f:
|
|
f.write(b"\xef\xbb\xbf" + bin_text.encode("utf-8"))
|
|
|
|
# ── metadata object file ─────────────────────────────────────
|
|
|
|
if not args.Synonym and meta_synonym:
|
|
syn_items = meta_synonym
|
|
elif args.Synonym:
|
|
syn_items = [{"Lang": "ru", "Content": args.Synonym}]
|
|
else:
|
|
syn_items = [{"Lang": "ru", "Content": name}]
|
|
comment = args.Comment or meta_comment or ""
|
|
|
|
md_lines = [
|
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
'<MetaDataObject xmlns="http://v8.1c.ru/8.3/MDClasses" xmlns:app="http://v8.1c.ru/8.2/managed-application/core" '
|
|
'xmlns:cfg="http://v8.1c.ru/8.1/data/enterprise/current-config" xmlns:cmi="http://v8.1c.ru/8.2/managed-application/cmi" '
|
|
'xmlns:ent="http://v8.1c.ru/8.1/data/enterprise" xmlns:lf="http://v8.1c.ru/8.2/managed-application/logform" '
|
|
'xmlns:style="http://v8.1c.ru/8.1/data/ui/style" xmlns:sys="http://v8.1c.ru/8.1/data/ui/fonts/system" '
|
|
'xmlns:v8="http://v8.1c.ru/8.1/data/core" xmlns:v8ui="http://v8.1c.ru/8.1/data/ui" '
|
|
'xmlns:web="http://v8.1c.ru/8.1/data/ui/colors/web" xmlns:win="http://v8.1c.ru/8.1/data/ui/colors/windows" '
|
|
'xmlns:xen="http://v8.1c.ru/8.3/xcf/enums" xmlns:xpr="http://v8.1c.ru/8.3/xcf/predef" '
|
|
'xmlns:xr="http://v8.1c.ru/8.3/xcf/readable" xmlns:xs="http://www.w3.org/2001/XMLSchema" '
|
|
'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.17">',
|
|
f'\t<XDTOPackage uuid="{uuid.uuid4()}">',
|
|
"\t\t<Properties>",
|
|
f"\t\t\t<Name>{esc_text(name)}</Name>",
|
|
"\t\t\t<Synonym>",
|
|
]
|
|
for s in syn_items:
|
|
md_lines += [
|
|
"\t\t\t\t<v8:item>",
|
|
f'\t\t\t\t\t<v8:lang>{esc_text(s["Lang"])}</v8:lang>',
|
|
f'\t\t\t\t\t<v8:content>{esc_text(s["Content"])}</v8:content>',
|
|
"\t\t\t\t</v8:item>",
|
|
]
|
|
md_lines.append("\t\t\t</Synonym>")
|
|
md_lines.append(f"\t\t\t<Comment>{esc_text(comment)}</Comment>" if comment else "\t\t\t<Comment/>")
|
|
md_lines.append(f"\t\t\t<Namespace>{esc_text(target_ns)}</Namespace>")
|
|
md_lines += ["\t\t</Properties>", "\t</XDTOPackage>", "</MetaDataObject>"]
|
|
|
|
with open(md_file, "wb") as f:
|
|
f.write(b"\xef\xbb\xbf" + "\r\n".join(md_lines).encode("utf-8"))
|
|
|
|
# ── register in Configuration.xml ────────────────────────────
|
|
|
|
# Ранняя диагностика: отказ платформы при db-update дешевле поймать на сборке
|
|
xdto_root_dir = os.path.join(args.OutputDir, "XDTOPackages")
|
|
declared_imports = [a["value"] for c in pkg_node.children if c.tag == "import"
|
|
for a in c.attrs if a["name"] == "namespace"]
|
|
if declared_imports and os.path.isdir(xdto_root_dir):
|
|
known_ns = set()
|
|
for other in sorted(os.listdir(xdto_root_dir)):
|
|
ob = os.path.join(xdto_root_dir, other, "Ext", "Package.bin")
|
|
if not os.path.exists(ob):
|
|
continue
|
|
try:
|
|
known_ns.add(_parse_xml(ob).getroot().get("targetNamespace"))
|
|
except Exception: # noqa: BLE001
|
|
pass
|
|
for imp in declared_imports:
|
|
if imp not in known_ns and imp not in PLATFORM_NS:
|
|
warn(f'Импорт "{imp}" не разрешается: пакета с таким namespace в конфигурации нет. '
|
|
"Платформа отвергнет пакет при обновлении — соберите зависимость первой")
|
|
|
|
config_xml = os.path.join(args.OutputDir, "Configuration.xml")
|
|
reg_result = "no-config"
|
|
if os.path.exists(config_xml):
|
|
with open(config_xml, "rb") as f:
|
|
raw = f.read()
|
|
had_bom = raw.startswith(b"\xef\xbb\xbf")
|
|
cfg_doc = _parse_xml(config_xml)
|
|
child_objects = cfg_doc.find(f".//{{{MD_NS}}}Configuration/{{{MD_NS}}}ChildObjects")
|
|
if child_objects is not None:
|
|
existing = child_objects.findall(f"{{{MD_NS}}}XDTOPackage")
|
|
if any((e.text or "") == name for e in existing):
|
|
reg_result = "already"
|
|
else:
|
|
new_elem = etree.SubElement(child_objects, f"{{{MD_NS}}}XDTOPackage")
|
|
new_elem.text = name
|
|
if existing:
|
|
last = existing[-1]
|
|
new_elem.tail = last.tail
|
|
child_objects.remove(new_elem)
|
|
last.addnext(new_elem)
|
|
else:
|
|
new_elem.tail = child_objects.text
|
|
data = etree.tostring(cfg_doc, xml_declaration=True, encoding="UTF-8")
|
|
if had_bom:
|
|
data = b"\xef\xbb\xbf" + data
|
|
with open(config_xml, "wb") as f:
|
|
f.write(data)
|
|
reg_result = "added"
|
|
|
|
type_count = sum(1 for c in pkg_node.children if c.tag in ("objectType", "valueType"))
|
|
print(f"✓ Пакет XDTO собран: {name}")
|
|
print(f" Namespace: {target_ns}")
|
|
print(f" Типов: {type_count}")
|
|
print(f" Файлы: XDTOPackages/{name}.xml, XDTOPackages/{name}/Ext/Package.bin")
|
|
if warnings_list:
|
|
print("")
|
|
print("Предупреждения (" + str(len(warnings_list)) +
|
|
") — конструкции XSD без точного соответствия в модели XDTO:")
|
|
for w in warnings_list:
|
|
print(" ! " + w)
|
|
print("")
|
|
if reg_result == "added":
|
|
print(f" Configuration.xml: <XDTOPackage>{name}</XDTOPackage> добавлен в ChildObjects")
|
|
elif reg_result == "already":
|
|
print(f" Configuration.xml: <XDTOPackage>{name}</XDTOPackage> уже зарегистрирован")
|
|
else:
|
|
print(" Configuration.xml не найден — регистрация пропущена")
|