From 24cfe6a0a662671161570347467c4ec72d5d98ae Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 6 Sep 2026 14:32:43 +0300 Subject: [PATCH] =?UTF-8?q?feat(meta-decompile):=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=B1=D0=BE=D1=80=20=D0=B2=D0=BD=D0=B5=D1=88=D0=BD=D0=B8=D1=85?= =?UTF-8?q?=20=D0=B8=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B8=20=D1=80=D0=B0?= =?UTF-8?q?=D1=83=D0=BD=D0=B4=D1=82=D1=80=D0=B8=D0=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Декомпилятор собирает DSL источника целиком: читает файл источника, подтягивает файлы таблиц из <Источник>/Tables/ и складывает поля, ключи, ссылки и функции обратно в тот же синтаксис, который принимает meta-compile. Ссылки на поля возвращаются короткими именами, таблица без собственных свойств — коротким массивом полей, функция с типом по умолчанию — одной строкой. Это замыкает дешёвый контур проверки: XML → декомпиляция → компиляция → сравнение с исходником, без 1С и без Docker. На нём и проверено: наша выгрузка, выгрузка платформы и четыре таблицы внешних источников из чужого рабочего проекта возвращаются байт в байт. У выгрузки платформы остаются два известных расхождения: значение незаполненного родителя (платформа сама сбрасывает его при любой загрузке) и формы (декомпилятор их не захватывает — так задумано). Побочно закрыт дефект, к внешним источникам не относящийся: xs:base64Binary разбирался как ХранилищеЗначения, хотя это ДвоичныеДанные. Различать их можно по квалификаторам — у ХранилищеЗначения платформа пишет v8:ValueStorage, а у двоичных данных есть BinaryDataQualifiers. Компилятор научен типу BinaryData (и BinaryData(N)) — до этого он принимал такое имя, но эмитил его как есть, то есть невалидный XML. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NBsZA5cr2WFThtgp7i5WVi --- .../reference/external-data-source.md | 5 +- .../meta-compile/scripts/meta-compile.ps1 | 17 +- .../meta-compile/scripts/meta-compile.py | 17 +- .../meta-decompile/scripts/meta-decompile.ps1 | 135 ++++++++- .../meta-decompile/scripts/meta-decompile.py | 171 +++++++++++- .../meta-decompile/external-data-source.json | 58 ++++ .../ExternalDataSources/PG.xml | 67 +++++ .../ExternalDataSources/PG/Tables/prices.xml | 170 ++++++++++++ .../PG/Tables/products.xml | 256 ++++++++++++++++++ .../snapshots/external-data-source/draft.json | 33 +++ 10 files changed, 920 insertions(+), 9 deletions(-) create mode 100644 tests/skills/cases/meta-decompile/external-data-source.json create mode 100644 tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG.xml create mode 100644 tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/prices.xml create mode 100644 tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/products.xml create mode 100644 tests/skills/cases/meta-decompile/snapshots/external-data-source/draft.json diff --git a/.claude/skills/meta-compile/reference/external-data-source.md b/.claude/skills/meta-compile/reference/external-data-source.md index a05445626..ce0800ada 100644 --- a/.claude/skills/meta-compile/reference/external-data-source.md +++ b/.claude/skills/meta-compile/reference/external-data-source.md @@ -97,8 +97,9 @@ ] ``` -Допустимые типы: `Number`, `String`, `Date`, `Boolean`, `UUID`, `BinaryData` и ссылка на таблицу -внешнего источника — `ExternalDataSourceTableRef.<Источник>.<Таблица>`. +Допустимые типы: `Number`, `String`, `Date`, `Boolean`, `UUID`, `BinaryData` (двоичные данные; +`BinaryData(N)` — с ограничением длины) и ссылка на таблицу внешнего источника — +`ExternalDataSourceTableRef.<Источник>.<Таблица>`. **Составной тип у поля недопустим** — платформа такую конфигурацию не загружает. diff --git a/.claude/skills/meta-compile/scripts/meta-compile.ps1 b/.claude/skills/meta-compile/scripts/meta-compile.ps1 index d82dbb021..a427b792f 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.ps1 +++ b/.claude/skills/meta-compile/scripts/meta-compile.ps1 @@ -1,4 +1,4 @@ -# meta-compile v1.106 — Compile 1C metadata object from JSON +# meta-compile v1.107 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills [CmdletBinding(PositionalBinding=$false)] param( @@ -604,6 +604,10 @@ $script:typeSynonyms["bool"] = "Boolean" # ValueStorage / UUID — прощающий ввод (модель может написать base64Binary / рус. форму → канон). $script:typeSynonyms["valuestorage"] = "ValueStorage" $script:typeSynonyms["base64binary"] = "ValueStorage" +# ДвоичныеДанные — ОТДЕЛЬНЫЙ тип, не ХранилищеЗначения: платформа пишет его как +# xs:base64Binary с квалификаторами. Встречается у полей внешних источников данных. +$script:typeSynonyms["binarydata"] = "BinaryData" +$script:typeSynonyms["двоичныеданные"] = "BinaryData" $script:typeSynonyms["хранилищезначений"] = "ValueStorage" $script:typeSynonyms["хранилищезначения"] = "ValueStorage" $script:typeSynonyms["uuid"] = "UUID" @@ -807,6 +811,17 @@ function Emit-TypeContent { } # ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary, хоть 1С и принимает оба). + # ДвоичныеДанные — xs:base64Binary с квалификаторами (у полей внешних источников). + if ($typeStr -eq "BinaryData" -or $typeStr -match '^BinaryData\(') { + $blen = if ($typeStr -match '^BinaryData\((\d+)\)$') { $Matches[1] } else { "4294967292" } + $ballowed = if ($typeStr -match '^BinaryData\(\d+\)$') { "Variable" } else { "Fixed" } + X "$indentxs:base64Binary" + X "$indent" + X "$indent`t$blen" + X "$indent`t$ballowed" + X "$indent" + return + } if ($typeStr -eq "ValueStorage") { X "$indentv8:ValueStorage" return diff --git a/.claude/skills/meta-compile/scripts/meta-compile.py b/.claude/skills/meta-compile/scripts/meta-compile.py index 7adbc4ef6..96ef5e955 100644 --- a/.claude/skills/meta-compile/scripts/meta-compile.py +++ b/.claude/skills/meta-compile/scripts/meta-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# meta-compile v1.106 — Compile 1C metadata object from JSON +# meta-compile v1.107 — Compile 1C metadata object from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -735,6 +735,10 @@ type_synonyms = { # ValueStorage / UUID — прощающий ввод (base64Binary / рус. форма → канон). 'valuestorage': 'ValueStorage', 'base64binary': 'ValueStorage', + # ДвоичныеДанные — ОТДЕЛЬНЫЙ тип, не ХранилищеЗначения: платформа пишет его как + # xs:base64Binary с квалификаторами. Встречается у полей внешних источников данных. + 'binarydata': 'BinaryData', + 'двоичныеданные': 'BinaryData', 'хранилищезначений': 'ValueStorage', 'хранилищезначения': 'ValueStorage', 'uuid': 'UUID', @@ -927,6 +931,17 @@ def emit_type_content(indent, type_str): if re.match(r'^(CatalogRef|DocumentRef|EnumRef|ChartOfAccountsRef|ChartOfCharacteristicTypesRef|ChartOfCalculationTypesRef|ExchangePlanRef|BusinessProcessRef|TaskRef|AnyRef|AnyIBRef)$', type_str): X(f'{indent}cfg:{type_str}') return + # ДвоичныеДанные — xs:base64Binary с квалификаторами (у полей внешних источников). + m_bin = re.match(r'^BinaryData(?:\((\d+)\))?$', type_str) + if m_bin: + blen = m_bin.group(1) or '4294967292' + ballowed = 'Variable' if m_bin.group(1) else 'Fixed' + X(f'{indent}xs:base64Binary') + X(f'{indent}') + X(f'{indent}\t{blen}') + X(f'{indent}\t{ballowed}') + X(f'{indent}') + return # ValueStorage (ХранилищеЗначения) — канон v8:ValueStorage (не xs:base64Binary). if type_str == 'ValueStorage': X(f'{indent}v8:ValueStorage') diff --git a/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 b/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 index 40699c2e7..920509beb 100644 --- a/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 +++ b/.claude/skills/meta-decompile/scripts/meta-decompile.ps1 @@ -1,4 +1,4 @@ -# meta-decompile v0.65 — XML объекта метаданных 1С → JSON-черновик формата meta-compile +# meta-decompile v0.66 — XML объекта метаданных 1С → JSON-черновик формата meta-compile # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # # Поддержаны: Catalog, ExchangePlan, ChartOfCharacteristicTypes, ChartOfAccounts, ChartOfCalculationTypes, Document, @@ -93,7 +93,7 @@ foreach ($c in $rootEl.ChildNodes) { if ($c.NodeType -eq 'Element') { $objNode = if (-not $objNode) { [Console]::Error.WriteLine("meta-decompile: пустой MetaDataObject"); exit 3 } $objType = $objNode.LocalName -if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService')) { +if ($objType -notin @('Catalog', 'ExchangePlan', 'ChartOfCharacteristicTypes', 'ChartOfAccounts', 'ChartOfCalculationTypes', 'Document', 'InformationRegister', 'AccumulationRegister', 'AccountingRegister', 'CalculationRegister', 'BusinessProcess', 'Task', 'Enum', 'Report', 'DataProcessor', 'Constant', 'DefinedType', 'FunctionalOption', 'DocumentJournal', 'Sequence', 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService', 'ExternalDataSource')) { [Console]::Error.WriteLine("meta-decompile: тип '$objType' пока не поддержан (…, CommonPicture, CommonTemplate)"); exit 3 } @@ -206,7 +206,18 @@ function Get-TypeShorthand { if ($dq) { $dn = $dq.SelectSingleNode('v8:DateFractions', $nsm); if ($dn) { $fr = $dn.InnerText } } $parts += $fr; break # Date | DateTime } - '(^|:)base64Binary$' { $parts += 'ValueStorage'; break } + '(^|:)base64Binary$' { + # xs:base64Binary — это ДвоичныеДанные, если рядом есть свои квалификаторы; + # ХранилищеЗначения платформа пишет как v8:ValueStorage, но принимает и эту форму. + $bq = $typeNode.SelectSingleNode('v8:BinaryDataQualifiers', $nsm) + if ($bq) { + $blen = $bq.SelectSingleNode('v8:Length', $nsm) + $bal = $bq.SelectSingleNode('v8:AllowedLength', $nsm) + if ($blen -and $bal -and $bal.InnerText -eq 'Variable') { $parts += "BinaryData($($blen.InnerText))" } + else { $parts += 'BinaryData' } + } else { $parts += 'ValueStorage' } + break + } default { $parts += (Strip-NsPrefix $raw) } # cfg:CatalogRef.X → CatalogRef.X } } elseif ($ln -eq 'TypeSet') { @@ -288,12 +299,19 @@ function Attr-ToDsl { param($attrNode) $ap = $attrNode.SelectSingleNode('md:Properties', $nsm) $nm = ($ap.SelectSingleNode('md:Name', $nsm)).InnerText + # Поле внешнего источника: три своих свойства. Имя колонки по умолчанию равно имени поля, + # поэтому в DSL попадает только отличающееся. + $edsNids = $ap.SelectSingleNode('md:NameInDataSource', $nsm) + $edsRo = $ap.SelectSingleNode('md:ReadOnly', $nsm) + $edsNull = $ap.SelectSingleNode('md:AllowNull', $nsm) $ts = Get-TypeShorthand ($ap.SelectSingleNode('md:Type', $nsm)) $flags = @() $fc = $ap.SelectSingleNode('md:FillChecking', $nsm); if ($fc -and $fc.InnerText -eq 'ShowError') { $flags += 'req' } $ix = $ap.SelectSingleNode('md:Indexing', $nsm) if ($ix) { if ($ix.InnerText -eq 'Index') { $flags += 'index' } elseif ($ix.InnerText -eq 'IndexWithAdditionalOrder') { $flags += 'indexAdditional' } } $ml = $ap.SelectSingleNode('md:MultiLine', $nsm); if ($ml -and $ml.InnerText -eq 'true') { $flags += 'multiline' } + if ($edsRo -and $edsRo.InnerText -eq 'true') { $flags += 'readonly' } + if ($edsNull -and $edsNull.InnerText -eq 'true') { $flags += 'nullable' } # Синоним/подсказка (строка ru-only ИЛИ {ru,en}). $synNode = $ap.SelectSingleNode('md:Synonym', $nsm) @@ -407,6 +425,7 @@ function Attr-ToDsl { # Пустой (реквизит без типа / произвольный) → $ts=''. Отличаем от «дефолтного» отсутствия: # заставляем объектную форму с явным type:'' (компилятор без маркера подставил бы xs:string). $typeEmpty = ($ts -eq '') + if ($edsNids -and $edsNids.InnerText -and $edsNids.InnerText -cne $nm) { $extra['nameInDataSource'] = $edsNids.InnerText } if ($synCustom -or $synEmpty -or ($null -ne $ttVal) -or $extra.Count -gt 0 -or $typeEmpty) { $o = [ordered]@{ name = $nm } if ($ts) { $o['type'] = $ts } elseif ($typeEmpty) { $o['type'] = '' } @@ -1658,6 +1677,116 @@ if ($objType -eq 'ExchangePlan') { } } +# --- Внешний источник данных: таблицы (отдельные файлы) и функции (узлы внутри файла) --- +if ($objType -eq 'ExternalDataSource') { + $dlcmVal = P 'DataLockControlMode' + if ($dlcmVal -and $dlcmVal -cne 'Automatic') { $dsl['dataLockControlMode'] = $dlcmVal } + + # Короткое имя из полного пути ExternalDataSource.И.Table.Т.Field.П + function Short-FieldRef { param([string]$ref) if ($ref) { return ($ref -split '\.')[-1] } else { return $null } } + function Field-RefList { param($parent, [string]$tag) + $out = [System.Collections.ArrayList]@() + foreach ($f in @($parent.SelectNodes("md:$tag/xr:Field", $nsm))) { [void]$out.Add((Short-FieldRef $f.InnerText)) } + # Запятая обязательна: return разворачивает коллекцию из одного элемента в скаляр, + # и список ключевых полей из одного поля уехал бы в JSON строкой вместо массива. + return ,$out + } + + $srcDir = Join-Path (Split-Path -Parent (Resolve-Path -LiteralPath $ObjectPath).Path) $objName + $childObjsEds = $objNode.SelectSingleNode('md:ChildObjects', $nsm) + if ($childObjsEds) { + $tablesMap = [ordered]@{} + foreach ($tNode in @($childObjsEds.SelectNodes('md:Table', $nsm))) { + $tblName = $tNode.InnerText.Trim() + $tblPath = Join-Path (Join-Path $srcDir 'Tables') "$tblName.xml" + if (-not (Test-Path -LiteralPath $tblPath)) { + [Console]::Error.WriteLine("meta-decompile: файл таблицы не найден: $tblPath") + continue + } + $tdoc = New-Object System.Xml.XmlDocument + $tdoc.PreserveWhitespace = $true + $tdoc.Load($tblPath) + $tObjNode = $null + foreach ($c in $tdoc.DocumentElement.ChildNodes) { if ($c.NodeType -eq 'Element') { $tObjNode = $c; break } } + $tp = $tObjNode.SelectSingleNode('md:Properties', $nsm) + function TP { param([string]$tag) $n = $tp.SelectSingleNode("md:$tag", $nsm); if ($n) { return $n.InnerText } else { return $null } } + + $tbl = [ordered]@{} + $tSynNode = $tp.SelectSingleNode('md:Synonym', $nsm) + $tSyn = Get-MLValue $tSynNode + if ($tSyn -is [string]) { if ($tSyn -cne (Split-CamelWords $tblName)) { $tbl['synonym'] = $tSyn } } + elseif ($null -ne $tSyn) { $tbl['synonym'] = $tSyn } + # Пустой ≠ авто-синоним из имени: без явного '' компилятор до-генерит его из имени. + elseif ($tSynNode) { $tbl['synonym'] = '' } + $tCmt = TP 'Comment'; if ($tCmt) { $tbl['comment'] = $tCmt } + $tType = TP 'TableType'; if ($tType -and $tType -cne 'Table') { $tbl['tableType'] = $tType } + $nids = TP 'NameInDataSource'; if ($nids -and $nids -cne $tblName) { $tbl['nameInDataSource'] = $nids } + $expr = TP 'ExpressionInDataSource'; if ($expr) { $tbl['expressionInDataSource'] = $expr } + $tdt = TP 'TableDataType'; if ($tdt -and $tdt -cne 'NonobjectData') { $tbl['tableDataType'] = $tdt } + $keys = Field-RefList $tp 'KeyFields'; if ($keys.Count -gt 0) { $tbl['keyFields'] = $keys } + foreach ($pair in @(@('PresentationField','presentationField'), @('ParentField','parentField'), @('DataVersionField','dataVersionField'))) { + $v = Short-FieldRef (TP $pair[0]); if ($v) { $tbl[$pair[1]] = $v } + } + $ibs = Field-RefList $tp 'InputByString' + # Ввод по строке компилятор выводит из поля представления: совпадающий список не пишем. + $ibsAuto = if ($tbl['presentationField']) { @($tbl['presentationField']) } else { @() } + if (($ibs -join ',') -cne ($ibsAuto -join ',')) { $tbl['inputByString'] = $ibs } + $dlf = Field-RefList $tp 'DataLockFields'; if ($dlf.Count -gt 0) { $tbl['dataLockFields'] = $dlf } + if ((TP 'ReadOnly') -eq 'true') { $tbl['readOnly'] = $true } + $til = TP 'TransactionsIsolationLevel'; if ($til -and $til -cne 'Auto') { $tbl['transactionsIsolationLevel'] = $til } + $tdlcm = TP 'DataLockControlMode'; if ($tdlcm -and $tdlcm -cne 'Automatic') { $tbl['dataLockControlMode'] = $tdlcm } + if ((TP 'UseStandardCommands') -eq 'false') { $tbl['useStandardCommands'] = $false } + if ((TP 'QuickChoice') -eq 'true') { $tbl['quickChoice'] = $true } + $tet = TP 'EditType'; if ($tet -and $tet -cne 'InDialog') { $tbl['editType'] = $tet } + $basedOn = [System.Collections.ArrayList]@() + foreach ($it in @($tp.SelectNodes('md:BasedOn/xr:Item', $nsm))) { [void]$basedOn.Add($it.InnerText) } + if ($basedOn.Count -gt 0) { $tbl['basedOn'] = $basedOn } + + $fieldsArr = [System.Collections.ArrayList]@() + $tChild = $tObjNode.SelectSingleNode('md:ChildObjects', $nsm) + if ($tChild) { + foreach ($f in @($tChild.SelectNodes('md:Field', $nsm))) { [void]$fieldsArr.Add((Attr-ToDsl $f)) } + } + # Таблица без собственных свойств — короткая форма: просто массив полей. + if ($tbl.Count -eq 0) { $tablesMap[$tblName] = $fieldsArr } + else { $tbl['fields'] = $fieldsArr; $tablesMap[$tblName] = $tbl } + } + if ($tablesMap.Count -gt 0) { $dsl['tables'] = $tablesMap } + + $fnMap = [ordered]@{} + foreach ($fnNode in @($childObjsEds.SelectNodes('md:Function', $nsm))) { + $fp = $fnNode.SelectSingleNode('md:Properties', $nsm) + $fnName = ($fp.SelectSingleNode('md:Name', $nsm)).InnerText + $fnExprNode = $fp.SelectSingleNode('md:ExpressionInDataSource', $nsm) + $fnExpr = if ($fnExprNode) { $fnExprNode.InnerText } else { '' } + $fnRetNode = $fp.SelectSingleNode('md:ReturnValue', $nsm) + $fnReturns = Get-TypeShorthand ($fp.SelectSingleNode('md:Type', $nsm)) + $fnSyn = Get-MLValue ($fp.SelectSingleNode('md:Synonym', $nsm)) + $fnCmtNode = $fp.SelectSingleNode('md:Comment', $nsm) + $fnCmt = if ($fnCmtNode) { $fnCmtNode.InnerText } else { '' } + $fnNoValue = ($fnRetNode -and $fnRetNode.InnerText -eq 'false') + $synCustomFn = ($fnSyn -isnot [string]) -and ($null -ne $fnSyn) + if ($fnSyn -is [string]) { $synCustomFn = ($fnSyn -cne (Split-CamelWords $fnName)) -and ($fnSyn -ne '') } + # Умолчание `returns` компилятора — String, а он даёт String(10): с ним и сверяем, + # иначе короткая форма (одна строка выражения) не срабатывала бы никогда. + if (-not $fnNoValue -and -not $fnCmt -and -not $synCustomFn -and ($fnReturns -cne 'String(10)')) { + $fo = [ordered]@{ expression = $fnExpr; returns = $fnReturns } + $fnMap[$fnName] = $fo + } elseif (-not $fnNoValue -and -not $fnCmt -and -not $synCustomFn) { + # Тип по умолчанию String — короткая форма: одна строка выражения. + $fnMap[$fnName] = $fnExpr + } else { + $fo = [ordered]@{ expression = $fnExpr } + if ($fnNoValue) { $fo['returnValue'] = $false } elseif ($fnReturns) { $fo['returns'] = $fnReturns } + if ($synCustomFn) { $fo['synonym'] = $fnSyn } + if ($fnCmt) { $fo['comment'] = $fnCmt } + $fnMap[$fnName] = $fo + } + } + if ($fnMap.Count -gt 0) { $dsl['functions'] = $fnMap } + } +} + # === Вывод === $json = ConvertTo-CompactJson $dsl 0 if ($OutputPath) { diff --git a/.claude/skills/meta-decompile/scripts/meta-decompile.py b/.claude/skills/meta-decompile/scripts/meta-decompile.py index faa2eba14..0087922a0 100644 --- a/.claude/skills/meta-decompile/scripts/meta-decompile.py +++ b/.claude/skills/meta-decompile/scripts/meta-decompile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# meta-decompile v0.65 — XML объекта метаданных 1С → JSON-черновик формата meta-compile +# meta-decompile v0.66 — XML объекта метаданных 1С → JSON-черновик формата meta-compile # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills # # Зеркало meta-decompile.ps1 (КАНОН). Структура 1:1 — те же имена функций, порядок, комментарии. @@ -311,7 +311,18 @@ def get_type_shorthand(type_node): fr = _text(dn) parts.append(fr) # Date | DateTime elif re.search(r'(^|:)base64Binary$', raw, re.I): - parts.append('ValueStorage') + # xs:base64Binary — это ДвоичныеДанные, если рядом есть свои квалификаторы; + # ХранилищеЗначения платформа пишет как v8:ValueStorage, но принимает и эту форму. + bq = type_node.find('v8:BinaryDataQualifiers', NS) + if bq is not None: + blen = bq.find('v8:Length', NS) + bal = bq.find('v8:AllowedLength', NS) + if blen is not None and bal is not None and _text(bal) == 'Variable': + parts.append(f'BinaryData({_text(blen)})') + else: + parts.append('BinaryData') + else: + parts.append('ValueStorage') else: parts.append(strip_ns_prefix(raw)) # cfg:CatalogRef.X → CatalogRef.X elif ln == 'TypeSet': @@ -420,6 +431,11 @@ def parse_choice_parameters(parent, tag): def attr_to_dsl(attr_node): ap = _single(attr_node, 'md:Properties') nm = _text(_single(ap, 'md:Name')) + # Поле внешнего источника: три своих свойства. Имя колонки по умолчанию равно имени поля, + # поэтому в DSL попадает только отличающееся. + eds_nids = _single(ap, 'md:NameInDataSource') + eds_ro = _single(ap, 'md:ReadOnly') + eds_null = _single(ap, 'md:AllowNull') ts = get_type_shorthand(_single(ap, 'md:Type')) flags = [] fc = _single(ap, 'md:FillChecking') @@ -435,6 +451,10 @@ def attr_to_dsl(attr_node): ml = _single(ap, 'md:MultiLine') if ml is not None and _text(ml) == 'true': flags.append('multiline') + if eds_ro is not None and _text(eds_ro) == 'true': + flags.append('readonly') + if eds_null is not None and _text(eds_null) == 'true': + flags.append('nullable') # Синоним/подсказка (строка ru-only ИЛИ {ru,en}). syn_node = _single(ap, 'md:Synonym') @@ -618,6 +638,8 @@ def attr_to_dsl(attr_node): # Пустой (реквизит без типа) → ts=''. Отличаем от «дефолтного» отсутствия: явный type:''. type_empty = (ts == '') + if eds_nids is not None and _text(eds_nids) and _text(eds_nids) != nm: + extra['nameInDataSource'] = _text(eds_nids) if syn_custom or syn_empty or (tt_val is not None) or len(extra) > 0 or type_empty: o = {'name': nm} if ts: @@ -2112,6 +2134,7 @@ SUPPORTED_TYPES = ( 'FilterCriterion', 'DocumentNumerator', 'SettingsStorage', 'CommonModule', 'EventSubscription', 'ScheduledJob', 'CommonForm', 'SessionParameter', 'CommonCommand', 'CommandGroup', 'CommonAttribute', 'FunctionalOptionsParameter', 'WSReference', 'CommonPicture', 'CommonTemplate', 'HTTPService', 'WebService', + 'ExternalDataSource', ) @@ -2154,6 +2177,150 @@ def main(): build_dsl() + # --- Внешний источник данных: таблицы (отдельные файлы) и функции (узлы внутри файла) --- + if obj_type == 'ExternalDataSource': + dlcm_val = P('DataLockControlMode') + if dlcm_val and dlcm_val != 'Automatic': + dsl['dataLockControlMode'] = dlcm_val + + def short_field_ref(ref): + """Короткое имя из полного пути ExternalDataSource.И.Table.Т.Field.П""" + return ref.split('.')[-1] if ref else None + + def field_ref_list(parent, tag): + return [short_field_ref(_text(f)) for f in parent.findall('md:%s/xr:Field' % tag, NS)] + + src_dir = os.path.join(os.path.dirname(os.path.abspath(args.ObjectPath)), obj_name) + child_objs_eds = _single(obj_node, 'md:ChildObjects') + if child_objs_eds is not None: + tables_map = {} + for t_node in child_objs_eds.findall('md:Table', NS): + tbl_name = (_text(t_node) or '').strip() + tbl_path = os.path.join(src_dir, 'Tables', tbl_name + '.xml') + if not os.path.isfile(tbl_path): + sys.stderr.write("meta-decompile: файл таблицы не найден: %s\n" % tbl_path) + continue + t_root = etree.parse(tbl_path).getroot() + t_obj_node = next((c for c in t_root if isinstance(c.tag, str)), None) + tp = _single(t_obj_node, 'md:Properties') + + def TP(tag, _tp=None): + n = _single(_tp if _tp is not None else tp, 'md:%s' % tag) + return _text(n) if n is not None else None + + tbl = {} + t_syn_node = _single(tp, 'md:Synonym') + t_syn = get_ml_value(t_syn_node) + if isinstance(t_syn, str): + if t_syn != split_camel_words(tbl_name): + tbl['synonym'] = t_syn + elif t_syn is not None: + tbl['synonym'] = t_syn + elif t_syn_node is not None: + # Пустой != авто-синоним из имени: без явного '' компилятор до-генерит его. + tbl['synonym'] = '' + t_cmt = TP('Comment') + if t_cmt: + tbl['comment'] = t_cmt + t_type = TP('TableType') + if t_type and t_type != 'Table': + tbl['tableType'] = t_type + nids = TP('NameInDataSource') + if nids and nids != tbl_name: + tbl['nameInDataSource'] = nids + expr = TP('ExpressionInDataSource') + if expr: + tbl['expressionInDataSource'] = expr + tdt = TP('TableDataType') + if tdt and tdt != 'NonobjectData': + tbl['tableDataType'] = tdt + keys = field_ref_list(tp, 'KeyFields') + if keys: + tbl['keyFields'] = keys + for tag, key in (('PresentationField', 'presentationField'), ('ParentField', 'parentField'), + ('DataVersionField', 'dataVersionField')): + v = short_field_ref(TP(tag)) + if v: + tbl[key] = v + ibs = field_ref_list(tp, 'InputByString') + # Ввод по строке компилятор выводит из поля представления: совпадающий список не пишем. + ibs_auto = [tbl['presentationField']] if tbl.get('presentationField') else [] + if ibs != ibs_auto: + tbl['inputByString'] = ibs + dlf = field_ref_list(tp, 'DataLockFields') + if dlf: + tbl['dataLockFields'] = dlf + if TP('ReadOnly') == 'true': + tbl['readOnly'] = True + til = TP('TransactionsIsolationLevel') + if til and til != 'Auto': + tbl['transactionsIsolationLevel'] = til + tdlcm = TP('DataLockControlMode') + if tdlcm and tdlcm != 'Automatic': + tbl['dataLockControlMode'] = tdlcm + if TP('UseStandardCommands') == 'false': + tbl['useStandardCommands'] = False + if TP('QuickChoice') == 'true': + tbl['quickChoice'] = True + t_et = TP('EditType') + if t_et and t_et != 'InDialog': + tbl['editType'] = t_et + based_on = [_text(it) for it in tp.findall('md:BasedOn/xr:Item', NS)] + if based_on: + tbl['basedOn'] = based_on + + fields_arr = [] + t_child = _single(t_obj_node, 'md:ChildObjects') + if t_child is not None: + for f in t_child.findall('md:Field', NS): + fields_arr.append(attr_to_dsl(f)) + # Таблица без собственных свойств — короткая форма: просто массив полей. + if not tbl: + tables_map[tbl_name] = fields_arr + else: + tbl['fields'] = fields_arr + tables_map[tbl_name] = tbl + if tables_map: + dsl['tables'] = tables_map + + fn_map = {} + for fn_node in child_objs_eds.findall('md:Function', NS): + fp = _single(fn_node, 'md:Properties') + fn_name = _text(_single(fp, 'md:Name')) + fn_expr_node = _single(fp, 'md:ExpressionInDataSource') + fn_expr = _text(fn_expr_node) if fn_expr_node is not None else '' + fn_ret_node = _single(fp, 'md:ReturnValue') + fn_returns = get_type_shorthand(_single(fp, 'md:Type')) + fn_syn = get_ml_value(_single(fp, 'md:Synonym')) + fn_cmt_node = _single(fp, 'md:Comment') + fn_cmt = _text(fn_cmt_node) if fn_cmt_node is not None else '' + fn_no_value = fn_ret_node is not None and _text(fn_ret_node) == 'false' + if isinstance(fn_syn, str): + syn_custom_fn = fn_syn != split_camel_words(fn_name) and fn_syn != '' + else: + syn_custom_fn = fn_syn is not None + # Умолчание `returns` компилятора — String, а он даёт String(10): с ним и сверяем, + # иначе короткая форма (одна строка выражения) не срабатывала бы никогда. + if not fn_no_value and not fn_cmt and not syn_custom_fn and fn_returns != 'String(10)': + fn_map[fn_name] = {'expression': fn_expr, 'returns': fn_returns} + elif not fn_no_value and not fn_cmt and not syn_custom_fn: + # Тип по умолчанию String — короткая форма: одна строка выражения. + fn_map[fn_name] = fn_expr + else: + fo = {'expression': fn_expr} + if fn_no_value: + fo['returnValue'] = False + elif fn_returns: + fo['returns'] = fn_returns + if syn_custom_fn: + fo['synonym'] = fn_syn + if fn_cmt: + fo['comment'] = fn_cmt + fn_map[fn_name] = fo + if fn_map: + dsl['functions'] = fn_map + + # === Вывод === json_str = convert_to_compact_json(dsl, 0) if args.OutputPath: diff --git a/tests/skills/cases/meta-decompile/external-data-source.json b/tests/skills/cases/meta-decompile/external-data-source.json new file mode 100644 index 000000000..c0361b1e6 --- /dev/null +++ b/tests/skills/cases/meta-decompile/external-data-source.json @@ -0,0 +1,58 @@ +{ + "name": "Декомпиляция внешнего источника: таблицы из отдельных файлов, функции, поля", + "preRun": [ + { + "script": "meta-compile/scripts/meta-compile", + "input": { + "type": "ExternalDataSource", + "name": "PG", + "dataLockControlMode": "AutomaticAndManaged", + "tables": { + "prices": ["product_id: Number(10,0)", "price: Number(15,2)"], + "products": { + "nameInDataSource": "eds.public.products", + "tableDataType": "ObjectData", + "keyFields": ["id"], + "presentationField": "name", + "transactionsIsolationLevel": "ReadUncommitted", + "fields": [ + "id: Number(10,0)", + "name: String(150)", + "photo: BinaryData | nullable", + { "name": "cost", "type": "Number(15,2)", "nameInDataSource": "cost_net", "readOnly": true } + ] + } + }, + "functions": { + "total": { "expression": "public.f_total(&1, &2)", "returns": "Number(15,2)" }, + "nextKey": "NEXT VALUE FOR public.seq_key" + } + }, + "args": { "-JsonPath": "{inputFile}", "-OutputDir": "{workDir}" } + } + ], + "params": { + "objectPath": "ExternalDataSources/PG.xml", + "outputPath": "draft.json" + }, + "expect": { + "files": ["draft.json"], + "fileContains": [ + { + "file": "draft.json", + "text": [ + "\"dataLockControlMode\": \"AutomaticAndManaged\"", + "\"keyFields\": [\"id\"]", + "\"nameInDataSource\": \"eds.public.products\"", + "\"transactionsIsolationLevel\": \"ReadUncommitted\"", + "photo: BinaryData | nullable", + "\"nameInDataSource\": \"cost_net\"", + "\"nextKey\": \"NEXT VALUE FOR public.seq_key\"" + ] + } + ], + "fileNotContains": [ + { "file": "draft.json", "text": "ValueStorage" } + ] + } +} diff --git a/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG.xml b/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG.xml new file mode 100644 index 000000000..ce6f9c996 --- /dev/null +++ b/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG.xml @@ -0,0 +1,67 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + + PG + + + ru + PG + + + + AutomaticAndManaged + + + prices
+ products
+ + + total + + + true + + xs:decimal + + 15 + 2 + Any + + + public.f_total(&1, &2) + + + + + nextKey + + + true + + xs:string + + 10 + Variable + + + NEXT VALUE FOR public.seq_key + + +
+
+
\ No newline at end of file diff --git a/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/prices.xml b/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/prices.xml new file mode 100644 index 000000000..f3d281122 --- /dev/null +++ b/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/prices.xml @@ -0,0 +1,170 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + UUID-012 + UUID-013 + + + UUID-014 + UUID-015 + + + UUID-016 + UUID-017 + + + + prices + + + ru + prices + + + + Table + prices + + NonobjectData + + + + + + true + false + + Auto + Begin + Directly + Auto + + + + + + + + + + + + false + false + Auto + + InDialog + + + Automatic + + + + + product_id + + + ru + product_id + + + + + xs:decimal + + 10 + 0 + Any + + + false + + + + false + + false + false + + + false + 0 + DontCheck + + + Auto + Auto + Auto + + product_id + false + false + + + + + price + + + ru + price + + + + + xs:decimal + + 15 + 2 + Any + + + false + + + + false + + false + false + + + false + 0 + DontCheck + + + Auto + Auto + Auto + + price + false + false + + + +
+
\ No newline at end of file diff --git a/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/products.xml b/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/products.xml new file mode 100644 index 000000000..f68a791a1 --- /dev/null +++ b/tests/skills/cases/meta-decompile/snapshots/external-data-source/ExternalDataSources/PG/Tables/products.xml @@ -0,0 +1,256 @@ + + + + + + UUID-002 + UUID-003 + + + UUID-004 + UUID-005 + + + UUID-006 + UUID-007 + + + UUID-008 + UUID-009 + + + UUID-010 + UUID-011 + + + UUID-012 + UUID-013 + + + UUID-014 + UUID-015 + + + UUID-016 + UUID-017 + + + + products + + + ru + products + + + + Table + eds.public.products + + ObjectData + + ExternalDataSource.PG.Table.products.Field.id + + ExternalDataSource.PG.Table.products.Field.name + + + + true + false + + ExternalDataSource.PG.Table.products.Field.name + + Auto + Begin + Directly + Auto + + + + + + + + + + + + false + false + ReadUncommitted + + InDialog + + + Automatic + + + + + id + + + ru + id + + + + + xs:decimal + + 10 + 0 + Any + + + false + + + + false + + false + false + + + false + 0 + DontCheck + + + Auto + Auto + Auto + + id + false + false + + + + + name + + + ru + name + + + + + xs:string + + 150 + Variable + + + false + + + + false + + false + false + + + false + + DontCheck + + + Auto + Auto + Auto + + name + false + false + + + + + photo + + + ru + photo + + + + + xs:base64Binary + + 4294967292 + Fixed + + + false + + + + false + + false + false + + + false + + DontCheck + + + Auto + Auto + Auto + + photo + false + true + + + + + cost + + + ru + cost + + + + + xs:decimal + + 15 + 2 + Any + + + false + + + + false + + false + false + + + false + 0 + DontCheck + + + Auto + Auto + Auto + + cost_net + true + false + + + +
+
\ No newline at end of file diff --git a/tests/skills/cases/meta-decompile/snapshots/external-data-source/draft.json b/tests/skills/cases/meta-decompile/snapshots/external-data-source/draft.json new file mode 100644 index 000000000..5bf91d417 --- /dev/null +++ b/tests/skills/cases/meta-decompile/snapshots/external-data-source/draft.json @@ -0,0 +1,33 @@ +{ + "type": "ExternalDataSource", + "name": "PG", + "dataLockControlMode": "AutomaticAndManaged", + "tables": { + "prices": ["product_id: Number(10,0)", "price: Number(15,2)"], + "products": { + "nameInDataSource": "eds.public.products", + "tableDataType": "ObjectData", + "keyFields": ["id"], + "presentationField": "name", + "transactionsIsolationLevel": "ReadUncommitted", + "fields": [ + "id: Number(10,0)", + "name: String(150)", + "photo: BinaryData | nullable", + { + "name": "cost", + "type": "Number(15,2)", + "nameInDataSource": "cost_net", + "flags": ["readonly"] + } + ] + } + }, + "functions": { + "total": { + "expression": "public.f_total(&1, &2)", + "returns": "Number(15,2)" + }, + "nextKey": "NEXT VALUE FOR public.seq_key" + } +} \ No newline at end of file