mirror of
https://github.com/Nikolay-Shirokov/cc-1c-skills.git
synced 2026-06-13 17:34:57 +03:00
feat(skd-compile): расширенный синтаксис role — shorthand + KV без whitelist
- Parse-RoleSpec (ps1+py): принимает string ("dim"/"flag1 flag2 K=V") / array / object
- Parse-FieldShorthand: извлекает K=V из shorthand поля (regex \w+=\S+)
- emit: токены → <dcscom:KEY>true</dcscom:KEY>; extras → <dcscom:KEY>VALUE</dcscom:KEY>
(без whitelist; раньше принимались только accountTypeExpression и balanceGroup)
- @period sugar поддерживает override через periodNumber/periodType KV
- Fix имени: balanceGroup в JSON принимается как deprecated alias для balanceGroupName
(в реальном XML 1С элемент называется balanceGroupName; старый код compile эмитил
несуществующий <dcscom:balanceGroup> — ни одного попадания в ERP-корпусе)
- SKILL.md, docs/skd-dsl-spec.md: единое описание четырёх форм роли
- v1.27 → v1.28
This commit is contained in:
@@ -94,7 +94,14 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/skd-compile.ps1" -V
|
||||
|
||||
Составной тип (несколько типов значений) — массив в объектной форме: `"type": ["CatalogRef.A", "CatalogRef.B"]`. Квалификаторы (`(N)`, `(D,F)`) применяются к каждому элементу.
|
||||
|
||||
Роли: `@dimension`, `@account`, `@balance`, `@period`.
|
||||
Роли (shorthand или объект):
|
||||
|
||||
- `@`-флаги: `@dimension`, `@account`, `@balance`, `@period`, `@required`, `@autoOrder`, `@ignoreNullValues`
|
||||
- KV: `balanceGroupName`, `balanceType` (`OpeningBalance`/`ClosingBalance`), `parentDimension`, `accountTypeExpression`, `expression`, `orderType` (`Asc`/`Desc`), `periodNumber`, `periodType`
|
||||
|
||||
```
|
||||
"Сумма: decimal(15,2) @balance balanceGroupName=Сумма balanceType=OpeningBalance"
|
||||
```
|
||||
|
||||
Ограничения: `#noField`, `#noFilter`, `#noGroup`, `#noOrder`.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# skd-compile v1.27 — Compile 1C DCS from JSON
|
||||
# skd-compile v1.28 — Compile 1C DCS from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
param(
|
||||
[string]$DefinitionFile,
|
||||
@@ -340,6 +340,7 @@ function Parse-FieldShorthand {
|
||||
$result = @{
|
||||
dataPath = ""; field = ""; title = ""; type = ""
|
||||
roles = @(); restrict = @(); appearance = [ordered]@{}
|
||||
roleExtras = [ordered]@{}
|
||||
}
|
||||
|
||||
# Extract @roles
|
||||
@@ -356,6 +357,11 @@ function Parse-FieldShorthand {
|
||||
}
|
||||
$s = [regex]::Replace($s, '\s*#\w+', '')
|
||||
|
||||
# Extract role kv=value (e.g. balanceGroupName=Сумма balanceType=OpeningBalance)
|
||||
$kvMatches = [regex]::Matches($s, '(\w+)=(\S+)')
|
||||
foreach ($m in $kvMatches) { $result.roleExtras[$m.Groups[1].Value] = $m.Groups[2].Value }
|
||||
$s = [regex]::Replace($s, '\s*\w+=\S+', '')
|
||||
|
||||
# Split name: type
|
||||
$s = $s.Trim()
|
||||
if ($s.Contains(':')) {
|
||||
@@ -370,6 +376,55 @@ function Parse-FieldShorthand {
|
||||
return $result
|
||||
}
|
||||
|
||||
# Universal role spec parser: string / array / object / null
|
||||
# Returns @{ tokens = @(...); extras = [ordered]@{...} }
|
||||
function Parse-RoleSpec {
|
||||
param($spec)
|
||||
$tokens = @()
|
||||
$extras = [ordered]@{}
|
||||
|
||||
if ($null -ne $spec) {
|
||||
if ($spec -is [string]) {
|
||||
if ($spec -notmatch '\s' -and $spec -notmatch '=') {
|
||||
$tokens += $spec
|
||||
} else {
|
||||
$s = $spec.Trim()
|
||||
foreach ($m in [regex]::Matches($s, '@(\w+)')) { $tokens += $m.Groups[1].Value }
|
||||
$s = [regex]::Replace($s, '\s*@\w+', '').Trim()
|
||||
foreach ($m in [regex]::Matches($s, '(\w+)=(\S+)')) { $extras[$m.Groups[1].Value] = $m.Groups[2].Value }
|
||||
}
|
||||
} elseif ($spec -is [array] -or $spec -is [System.Collections.IList]) {
|
||||
foreach ($t in $spec) { $tokens += "$t" }
|
||||
} elseif ($spec.PSObject -and $spec.PSObject.Properties) {
|
||||
foreach ($prop in $spec.PSObject.Properties) {
|
||||
$val = $prop.Value
|
||||
if ($val -is [bool]) {
|
||||
if ($val) { $tokens += $prop.Name }
|
||||
} elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [string]) {
|
||||
$extras[$prop.Name] = "$val"
|
||||
}
|
||||
}
|
||||
} elseif ($spec -is [hashtable] -or $spec -is [System.Collections.IDictionary]) {
|
||||
foreach ($k in $spec.Keys) {
|
||||
$val = $spec[$k]
|
||||
if ($val -is [bool]) {
|
||||
if ($val) { $tokens += "$k" }
|
||||
} elseif ($val -is [int] -or $val -is [long] -or $val -is [double] -or $val -is [string]) {
|
||||
$extras["$k"] = "$val"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Deprecated alias: balanceGroup → balanceGroupName (старое имя в коде compile, в реальном XML — Name)
|
||||
if ($extras.Contains('balanceGroup') -and -not $extras.Contains('balanceGroupName')) {
|
||||
$extras['balanceGroupName'] = $extras['balanceGroup']
|
||||
$extras.Remove('balanceGroup')
|
||||
}
|
||||
|
||||
return @{ tokens = $tokens; extras = $extras }
|
||||
}
|
||||
|
||||
# --- 6. Total field shorthand parser ---
|
||||
|
||||
function Parse-TotalShorthand {
|
||||
@@ -687,18 +742,13 @@ function Emit-Field {
|
||||
roles = @()
|
||||
restrict = @()
|
||||
appearance = [ordered]@{}
|
||||
roleExtras = [ordered]@{}
|
||||
}
|
||||
# Parse role
|
||||
# Parse role (string shorthand / array / object — единый формат с /skd-edit set-field-role)
|
||||
if ($fieldDef.role) {
|
||||
if ($fieldDef.role -is [string]) {
|
||||
$f.roles = @($fieldDef.role)
|
||||
} else {
|
||||
# Object form — collect truthy keys
|
||||
$roleObj = $fieldDef.role
|
||||
foreach ($prop in $roleObj.PSObject.Properties) {
|
||||
if ($prop.Value -eq $true) { $f.roles += $prop.Name }
|
||||
}
|
||||
}
|
||||
$parsed = Parse-RoleSpec $fieldDef.role
|
||||
$f.roles = $parsed.tokens
|
||||
$f.roleExtras = $parsed.extras
|
||||
}
|
||||
# Parse restrictions
|
||||
if ($fieldDef.restrict) {
|
||||
@@ -717,10 +767,6 @@ function Emit-Field {
|
||||
if ($fieldDef.attrRestrict) {
|
||||
$f["attrRestrict"] = @($fieldDef.attrRestrict)
|
||||
}
|
||||
# role object extras
|
||||
if ($fieldDef.role -and $fieldDef.role -isnot [string]) {
|
||||
$f["roleObj"] = $fieldDef.role
|
||||
}
|
||||
}
|
||||
|
||||
X "$indent<field xsi:type=`"DataSetFieldField`">"
|
||||
@@ -761,24 +807,23 @@ function Emit-Field {
|
||||
}
|
||||
|
||||
# Role
|
||||
if ($f.roles.Count -gt 0 -or $f["roleObj"]) {
|
||||
$hasExtras = $f["roleExtras"] -and $f["roleExtras"].Count -gt 0
|
||||
if ($f.roles.Count -gt 0 -or $hasExtras) {
|
||||
X "$indent`t<role>"
|
||||
foreach ($role in $f.roles) {
|
||||
if ($role -eq "period") {
|
||||
# @period -> periodNumber + periodType (not <dcscom:period>)
|
||||
X "$indent`t`t<dcscom:periodNumber>1</dcscom:periodNumber>"
|
||||
X "$indent`t`t<dcscom:periodType>Main</dcscom:periodType>"
|
||||
# @period — sugar для periodNumber=1 + periodType=Main; extras могут переопределить.
|
||||
$pnInExtras = $hasExtras -and $f["roleExtras"].Contains('periodNumber')
|
||||
$ptInExtras = $hasExtras -and $f["roleExtras"].Contains('periodType')
|
||||
if (-not $pnInExtras) { X "$indent`t`t<dcscom:periodNumber>1</dcscom:periodNumber>" }
|
||||
if (-not $ptInExtras) { X "$indent`t`t<dcscom:periodType>Main</dcscom:periodType>" }
|
||||
} else {
|
||||
X "$indent`t`t<dcscom:$role>true</dcscom:$role>"
|
||||
}
|
||||
}
|
||||
if ($f["roleObj"]) {
|
||||
$ro = $f["roleObj"]
|
||||
if ($ro.accountTypeExpression) {
|
||||
X "$indent`t`t<dcscom:accountTypeExpression>$(Esc-Xml "$($ro.accountTypeExpression)")</dcscom:accountTypeExpression>"
|
||||
}
|
||||
if ($ro.balanceGroup) {
|
||||
X "$indent`t`t<dcscom:balanceGroup>$(Esc-Xml "$($ro.balanceGroup)")</dcscom:balanceGroup>"
|
||||
if ($hasExtras) {
|
||||
foreach ($k in $f["roleExtras"].Keys) {
|
||||
X "$indent`t`t<dcscom:$k>$(Esc-Xml "$($f["roleExtras"][$k])")</dcscom:$k>"
|
||||
}
|
||||
}
|
||||
X "$indent`t</role>"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# skd-compile v1.26 — Compile 1C DCS from JSON
|
||||
# skd-compile v1.28 — Compile 1C DCS from JSON
|
||||
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
|
||||
import argparse
|
||||
import json
|
||||
@@ -218,6 +218,7 @@ def parse_field_shorthand(s):
|
||||
result = {
|
||||
'dataPath': '', 'field': '', 'title': '', 'type': '',
|
||||
'roles': [], 'restrict': [], 'appearance': {},
|
||||
'roleExtras': {},
|
||||
}
|
||||
|
||||
# Extract @roles
|
||||
@@ -232,6 +233,11 @@ def parse_field_shorthand(s):
|
||||
result['restrict'].append(m)
|
||||
s = re.sub(r'\s*#\w+', '', s)
|
||||
|
||||
# Extract role kv=value (e.g. balanceGroupName=Сумма)
|
||||
for m in re.finditer(r'(\w+)=(\S+)', s):
|
||||
result['roleExtras'][m.group(1)] = m.group(2)
|
||||
s = re.sub(r'\s*\w+=\S+', '', s)
|
||||
|
||||
# Split name: type
|
||||
s = s.strip()
|
||||
if ':' in s:
|
||||
@@ -245,6 +251,42 @@ def parse_field_shorthand(s):
|
||||
return result
|
||||
|
||||
|
||||
# Universal role spec parser: string / list / dict / None
|
||||
# Returns {'tokens': [...], 'extras': {...}}
|
||||
def parse_role_spec(spec):
|
||||
tokens = []
|
||||
extras = {}
|
||||
|
||||
if spec is None:
|
||||
pass
|
||||
elif isinstance(spec, str):
|
||||
if ' ' not in spec and '=' not in spec:
|
||||
tokens.append(spec)
|
||||
else:
|
||||
s = spec.strip()
|
||||
for m in re.finditer(r'@(\w+)', s):
|
||||
tokens.append(m.group(1))
|
||||
s = re.sub(r'\s*@\w+', '', s).strip()
|
||||
for m in re.finditer(r'(\w+)=(\S+)', s):
|
||||
extras[m.group(1)] = m.group(2)
|
||||
elif isinstance(spec, list):
|
||||
for t in spec:
|
||||
tokens.append(str(t))
|
||||
elif isinstance(spec, dict):
|
||||
for k, v in spec.items():
|
||||
if isinstance(v, bool):
|
||||
if v:
|
||||
tokens.append(k)
|
||||
elif isinstance(v, (int, float, str)):
|
||||
extras[k] = str(v)
|
||||
|
||||
# Deprecated alias: balanceGroup → balanceGroupName
|
||||
if 'balanceGroup' in extras and 'balanceGroupName' not in extras:
|
||||
extras['balanceGroupName'] = extras.pop('balanceGroup')
|
||||
|
||||
return {'tokens': tokens, 'extras': extras}
|
||||
|
||||
|
||||
# --- Total field shorthand parser ---
|
||||
|
||||
def parse_total_shorthand(s):
|
||||
@@ -523,16 +565,13 @@ def emit_field(lines, field_def, indent):
|
||||
'roles': [],
|
||||
'restrict': [],
|
||||
'appearance': {},
|
||||
'roleExtras': {},
|
||||
}
|
||||
# Parse role
|
||||
if field_def.get('role'):
|
||||
if isinstance(field_def['role'], str):
|
||||
f['roles'] = [field_def['role']]
|
||||
else:
|
||||
# Object form -- collect truthy keys
|
||||
for k, v in field_def['role'].items():
|
||||
if v is True:
|
||||
f['roles'].append(k)
|
||||
# Parse role (string shorthand / list / dict — единый формат с /skd-edit set-field-role)
|
||||
if field_def.get('role') is not None:
|
||||
parsed = parse_role_spec(field_def['role'])
|
||||
f['roles'] = parsed['tokens']
|
||||
f['roleExtras'] = parsed['extras']
|
||||
# Parse restrictions
|
||||
if field_def.get('restrict'):
|
||||
f['restrict'] = list(field_def['restrict'])
|
||||
@@ -545,9 +584,6 @@ def emit_field(lines, field_def, indent):
|
||||
# attrRestrict
|
||||
if field_def.get('attrRestrict'):
|
||||
f['attrRestrict'] = list(field_def['attrRestrict'])
|
||||
# role object extras
|
||||
if field_def.get('role') and not isinstance(field_def['role'], str):
|
||||
f['roleObj'] = field_def['role']
|
||||
|
||||
lines.append(f'{indent}<field xsi:type="DataSetFieldField">')
|
||||
lines.append(f'{indent}\t<dataPath>{esc_xml(f["dataPath"])}</dataPath>')
|
||||
@@ -580,20 +616,21 @@ def emit_field(lines, field_def, indent):
|
||||
lines.append(f'{indent}\t</attributeUseRestriction>')
|
||||
|
||||
# Role
|
||||
if (f.get('roles') and len(f['roles']) > 0) or f.get('roleObj'):
|
||||
extras = f.get('roleExtras') or {}
|
||||
has_extras = len(extras) > 0
|
||||
if (f.get('roles') and len(f['roles']) > 0) or has_extras:
|
||||
lines.append(f'{indent}\t<role>')
|
||||
for role in f.get('roles', []):
|
||||
if role == 'period':
|
||||
lines.append(f'{indent}\t\t<dcscom:periodNumber>1</dcscom:periodNumber>')
|
||||
lines.append(f'{indent}\t\t<dcscom:periodType>Main</dcscom:periodType>')
|
||||
# @period — sugar для periodNumber=1 + periodType=Main; extras могут переопределить.
|
||||
if 'periodNumber' not in extras:
|
||||
lines.append(f'{indent}\t\t<dcscom:periodNumber>1</dcscom:periodNumber>')
|
||||
if 'periodType' not in extras:
|
||||
lines.append(f'{indent}\t\t<dcscom:periodType>Main</dcscom:periodType>')
|
||||
else:
|
||||
lines.append(f'{indent}\t\t<dcscom:{role}>true</dcscom:{role}>')
|
||||
if f.get('roleObj'):
|
||||
ro = f['roleObj']
|
||||
if ro.get('accountTypeExpression'):
|
||||
lines.append(f'{indent}\t\t<dcscom:accountTypeExpression>{esc_xml(str(ro["accountTypeExpression"]))}</dcscom:accountTypeExpression>')
|
||||
if ro.get('balanceGroup'):
|
||||
lines.append(f'{indent}\t\t<dcscom:balanceGroup>{esc_xml(str(ro["balanceGroup"]))}</dcscom:balanceGroup>')
|
||||
for k, v in extras.items():
|
||||
lines.append(f'{indent}\t\t<dcscom:{k}>{esc_xml(str(v))}</dcscom:{k}>')
|
||||
lines.append(f'{indent}\t</role>')
|
||||
|
||||
# ValueType
|
||||
|
||||
+30
-15
@@ -124,7 +124,8 @@
|
||||
"Организация: CatalogRef.Организации @dimension",
|
||||
"Служебное: string #noFilter #noOrder",
|
||||
"Счёт: CatalogRef.Хозрасчетный @account",
|
||||
"Сумма: decimal(15,2) @balance"
|
||||
"Сумма: decimal(15,2) @balance",
|
||||
"СуммаНач: decimal(15,2) @balance balanceGroupName=Сумма balanceType=OpeningBalance"
|
||||
]
|
||||
```
|
||||
|
||||
@@ -146,9 +147,9 @@
|
||||
|
||||
### Парсинг shorthand
|
||||
|
||||
1. Разделить по пробелам; найти `@`-роли и `#`-ограничения
|
||||
1. Извлечь `@`-роли (regex `@(\w+)`), `#`-ограничения (`#(\w+)`), KV-пары роли (`(\w+)=(\S+)`)
|
||||
2. Остаток до первого `:` — `dataPath` (и `field` по умолчанию)
|
||||
3. После `:` до `@`/`#` — тип
|
||||
3. После `:` — тип
|
||||
|
||||
### Типы
|
||||
|
||||
@@ -192,22 +193,36 @@
|
||||
|
||||
### Роли
|
||||
|
||||
| DSL shorthand | Объектная форма | XML |
|
||||
|---------------|----------------|-----|
|
||||
| `@dimension` | `"role": "dimension"` или `{"dimension": true}` | `<dcscom:dimension>true</dcscom:dimension>` |
|
||||
| `@account` | `"role": "account"` или `{"account": true}` | `<dcscom:account>true</dcscom:account>` |
|
||||
| `@balance` | `"role": "balance"` или `{"balance": true}` | `<dcscom:balance>true</dcscom:balance>` |
|
||||
| `@period` | `"role": "period"` или `{"period": true}` | `<dcscom:periodNumber>1</dcscom:periodNumber>` + `<dcscom:periodType>Main</dcscom:periodType>` |
|
||||
Принимаются четыре формы:
|
||||
|
||||
Объектная форма с доп. полями:
|
||||
```json
|
||||
"role": {
|
||||
"account": true,
|
||||
"accountTypeExpression": "Счёт.ВидСчёта",
|
||||
"balanceGroup": "/Остатки"
|
||||
}
|
||||
"role": "dimension" // одиночный флаг
|
||||
"role": ["dimension", "required"] // массив флагов
|
||||
"role": "balance balanceGroupName=Сумма balanceType=OpeningBalance" // shorthand
|
||||
"role": { "balance": true, "balanceGroupName": "Сумма", "balanceType": "OpeningBalance" }
|
||||
```
|
||||
|
||||
Shorthand-формат может быть встроен прямо в shorthand поля:
|
||||
|
||||
```
|
||||
"Сумма: decimal(15,2) @balance balanceGroupName=Сумма balanceType=OpeningBalance"
|
||||
```
|
||||
|
||||
**Парсинг shorthand**: `@(\w+)` → boolean флаги; `(\w+)=(\S+)` → строковые KV; остаток — `dataPath[: type]`.
|
||||
|
||||
**Поддерживаемые ключи**:
|
||||
|
||||
| Категория | Ключи |
|
||||
|-----------|-------|
|
||||
| `@`-флаги (boolean) | `@dimension`, `@account`, `@balance`, `@period`, `@required`, `@autoOrder`, `@ignoreNullValues` |
|
||||
| Строковые KV | `balanceGroupName`, `balanceType` (`OpeningBalance`/`ClosingBalance`), `parentDimension`, `accountTypeExpression`, `expression`, `orderType` (`Asc`/`Desc`), `periodNumber`, `periodType` |
|
||||
|
||||
Whitelist'а нет — любой `<dcscom:KEY>` принимается; перечисленные — типичные. `@period` — sugar для `periodNumber=1` + `periodType=Main` (можно переопределить явно).
|
||||
|
||||
**XML-выход**: `<dcscom:KEY>true</dcscom:KEY>` для флагов; `<dcscom:KEY>VALUE</dcscom:KEY>` для KV.
|
||||
|
||||
> Устаревший ключ `balanceGroup` в object-форме принимается как alias для `balanceGroupName` (имя элемента в реальном XML — `balanceGroupName`).
|
||||
|
||||
### Ограничения
|
||||
|
||||
| DSL shorthand | Объектная форма | XML useRestriction |
|
||||
|
||||
Reference in New Issue
Block a user