feat(skd-compile): time type, string(N,fix), and composite type parameters

Calibrated against live Designer output in upload/erf/ПроверкаЭкранирования.

- New type 'time' (synonym 'время'): xs:dateTime with DateFractions=Time
  for time-of-day values. Designer uses the same xs:dateTime XSD type as
  date/dateTime — only DateFractions differs. Empty value: typed-zero
  0001-01-01T00:00:00 (same as dateTime).

- Extended string regex to accept (N,fix) → AllowedLength=Fixed (was
  Variable-only). Non-empty fixed-string values are emitted as-given
  without space-padding to Length — the platform handles padding on save.

- Composite types in parameters (array of types in object form, e.g.
  ["string(10,fix)", "CatalogRef.X"]) now work end-to-end: valueType
  emits each type with its qualifiers, and empty composite values
  serialize as <value xsi:nil="true"/> matching Designer.

Test case empty-param-values extended with 5 new params covering all
three additions. Snapshot validated by skd-validate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-05-20 12:52:26 +03:00
parent efdf56691c
commit ff2d8513c4
4 changed files with 119 additions and 20 deletions
@@ -1,4 +1,4 @@
# skd-compile v1.25 — Compile 1C DCS from JSON
# skd-compile v1.26 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
param(
[string]$DefinitionFile,
@@ -143,6 +143,7 @@ $script:typeSynonyms["строка"] = "string"
$script:typeSynonyms["булево"] = "boolean"
$script:typeSynonyms["дата"] = "date"
$script:typeSynonyms["датавремя"] = "dateTime"
$script:typeSynonyms["время"] = "time"
$script:typeSynonyms["стандартныйпериод"] = "StandardPeriod"
# English canonical (lowercase for lookup)
$script:typeSynonyms["bool"] = "boolean"
@@ -219,13 +220,14 @@ function Emit-SingleValueType {
return
}
# string or string(N)
if ($typeStr -match '^string(\((\d+)\))?$') {
# string, string(N), string(N,fix) — fix → AllowedLength=Fixed
if ($typeStr -match '^string(\((\d+)(,(fix|fixed))?\))?$') {
$len = if ($Matches[2]) { $Matches[2] } else { "0" }
$al = if ($Matches[4]) { "Fixed" } else { "Variable" }
X "$indent<v8:Type>xs:string</v8:Type>"
X "$indent<v8:StringQualifiers>"
X "$indent`t<v8:Length>$len</v8:Length>"
X "$indent`t<v8:AllowedLength>Variable</v8:AllowedLength>"
X "$indent`t<v8:AllowedLength>$al</v8:AllowedLength>"
X "$indent</v8:StringQualifiers>"
return
}
@@ -253,11 +255,12 @@ function Emit-SingleValueType {
return
}
# date / dateTime
if ($typeStr -match '^(date|dateTime)$') {
# date / dateTime / time — all use xs:dateTime, differ only in DateFractions
if ($typeStr -match '^(date|dateTime|time)$') {
$fractions = switch ($typeStr) {
"date" { "Date" }
"dateTime" { "DateTime" }
"time" { "Time" }
}
X "$indent<v8:Type>xs:dateTime</v8:Type>"
X "$indent<v8:DateQualifiers>"
@@ -996,7 +999,15 @@ function Emit-SingleParam {
# Value — for valueListAllowed params Designer omits <value> when empty
$vla = [bool]$parsed.valueListAllowed
Emit-ParamValue -type $parsed.type -val $parsed.value -indent "`t`t" -valueListAllowed $vla
if ($parsed.type -is [array] -or $parsed.type -is [System.Collections.IList]) {
# Composite type — Designer writes xsi:nil for any empty composite;
# non-empty composite values are uncommon and would need per-type tagging.
if (Test-EmptyValue $parsed.value) {
if (-not $vla) { X "`t`t<value xsi:nil=`"true`"/>" }
}
} else {
Emit-ParamValue -type $parsed.type -val $parsed.value -indent "`t`t" -valueListAllowed $vla
}
# Hidden implies useRestriction=true + availableAsField=false
if ($parsed.hidden -eq $true) {
@@ -1073,9 +1084,19 @@ function Emit-Parameters {
if ($p -is [string]) {
$parsed = Parse-ParamShorthand $p
} else {
# Composite type: ["string(10,fix)", "CatalogRef.X"] → array of resolved
# strings; emit-valueType handles arrays, empty value falls through to nil.
$resolvedType = ""
if ($p.type) {
if ($p.type -is [array] -or $p.type -is [System.Collections.IList]) {
$resolvedType = @($p.type | ForEach-Object { Resolve-TypeStr "$_" })
} else {
$resolvedType = Resolve-TypeStr "$($p.type)"
}
}
$parsed = @{
name = "$($p.name)"
type = if ($p.type) { Resolve-TypeStr "$($p.type)" } else { "" }
type = $resolvedType
value = $p.value
autoDates = $false
}
@@ -1148,7 +1169,7 @@ function Emit-EmptyValue {
X "$indent</${pf}value>"
} elseif ($t -match '^string') {
X "$indent<${pf}value xsi:type=`"xs:string`"/>"
} elseif ($t -match '^date') {
} elseif ($t -match '^(date|time)') {
X "$indent<${pf}value xsi:type=`"xs:dateTime`">0001-01-01T00:00:00</${pf}value>"
} elseif ($t -match '^decimal') {
X "$indent<${pf}value xsi:type=`"xs:decimal`">0</${pf}value>"
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# skd-compile v1.25 — Compile 1C DCS from JSON
# skd-compile v1.26 — Compile 1C DCS from JSON
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import argparse
import json
@@ -74,6 +74,7 @@ TYPE_SYNONYMS = {
"\u0431\u0443\u043b\u0435\u0432\u043e": "boolean",
"\u0434\u0430\u0442\u0430": "date",
"\u0434\u0430\u0442\u0430\u0432\u0440\u0435\u043c\u044f": "dateTime",
"\u0432\u0440\u0435\u043c\u044f": "time",
"\u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0439\u043f\u0435\u0440\u0438\u043e\u0434": "StandardPeriod",
# English canonical (lowercase)
"bool": "boolean",
@@ -148,14 +149,15 @@ def emit_single_value_type(lines, type_str, indent):
lines.append(f'{indent}<v8:Type>xs:boolean</v8:Type>')
return
# string or string(N)
m = re.match(r'^string(\((\d+)\))?$', type_str)
# string, string(N), string(N,fix) — fix → AllowedLength=Fixed
m = re.match(r'^string(\((\d+)(,(fix|fixed))?\))?$', type_str)
if m:
length = m.group(2) if m.group(2) else '0'
al = 'Fixed' if m.group(4) else 'Variable'
lines.append(f'{indent}<v8:Type>xs:string</v8:Type>')
lines.append(f'{indent}<v8:StringQualifiers>')
lines.append(f'{indent}\t<v8:Length>{length}</v8:Length>')
lines.append(f'{indent}\t<v8:AllowedLength>Variable</v8:AllowedLength>')
lines.append(f'{indent}\t<v8:AllowedLength>{al}</v8:AllowedLength>')
lines.append(f'{indent}</v8:StringQualifiers>')
return
@@ -181,10 +183,10 @@ def emit_single_value_type(lines, type_str, indent):
lines.append(f'{indent}</v8:NumberQualifiers>')
return
# date / dateTime
m = re.match(r'^(date|dateTime)$', type_str)
# date / dateTime / time — all use xs:dateTime, differ only in DateFractions
m = re.match(r'^(date|dateTime|time)$', type_str)
if m:
fractions_map = {'date': 'Date', 'dateTime': 'DateTime'}
fractions_map = {'date': 'Date', 'dateTime': 'DateTime', 'time': 'Time'}
fractions = fractions_map[type_str]
lines.append(f'{indent}<v8:Type>xs:dateTime</v8:Type>')
lines.append(f'{indent}<v8:DateQualifiers>')
@@ -827,7 +829,7 @@ def emit_empty_value(lines, type_str, indent, tag_prefix='', value_list_allowed=
lines.append(f'{indent}</{pf}value>')
elif re.match(r'^string', t):
lines.append(f'{indent}<{pf}value xsi:type="xs:string"/>')
elif re.match(r'^date', t):
elif re.match(r'^(date|time)', t):
lines.append(f'{indent}<{pf}value xsi:type="xs:dateTime">0001-01-01T00:00:00</{pf}value>')
elif re.match(r'^decimal', t):
lines.append(f'{indent}<{pf}value xsi:type="xs:decimal">0</{pf}value>')
@@ -898,7 +900,15 @@ def emit_single_param(lines, p, parsed):
# Value — for valueListAllowed params Designer omits <value> when empty
vla = bool(parsed.get('valueListAllowed'))
emit_param_value(lines, parsed.get('type', ''), parsed.get('value'), '\t\t', vla)
p_type = parsed.get('type', '')
if isinstance(p_type, (list, tuple)):
# Composite type — Designer writes xsi:nil for any empty composite;
# non-empty composite values are uncommon and would need per-type tagging.
if is_empty_value(parsed.get('value')):
if not vla:
lines.append('\t\t<value xsi:nil="true"/>')
else:
emit_param_value(lines, p_type, parsed.get('value'), '\t\t', vla)
# Hidden implies useRestriction=true + availableAsField=false
if parsed.get('hidden') is True:
@@ -971,9 +981,18 @@ def emit_parameters(lines, defn):
if isinstance(p, str):
parsed = parse_param_shorthand(p)
else:
# Composite type: ["string(10,fix)", "CatalogRef.X"] → list of resolved
# strings; emit_value_type handles lists, empty value falls through to nil.
raw_type = p.get('type')
if isinstance(raw_type, (list, tuple)):
resolved_type = [resolve_type_str(str(t)) for t in raw_type]
elif raw_type:
resolved_type = resolve_type_str(str(raw_type))
else:
resolved_type = ''
parsed = {
'name': str(p.get('name', '')),
'type': resolve_type_str(str(p['type'])) if p.get('type') else '',
'type': resolved_type,
'value': p.get('value'),
'autoDates': False,
}
@@ -17,7 +17,12 @@
"ПараметрБулево: boolean = ",
"ПараметрСтандартныйПериод: StandardPeriod = _",
{ "name": "ПараметрТипНеЗадан", "value": null },
"ПараметрСписокСтрок: string @valueList"
"ПараметрСписокСтрок: string @valueList",
"ПараметрВремяСЗначением: time = 0001-01-01T12:30:00",
"ПараметрВремяПусто: time",
"ПараметрСтрокаФиксСЗначением: string(10,fix) = АБВ",
"ПараметрСтрокаФиксПусто: string(10,fix)",
{ "name": "СоставнойТип", "type": ["string(10,fix)", "CatalogRef.ПлоскийПростой"], "value": null }
]
},
"validatePath": "Template.xml",
@@ -112,6 +112,60 @@
</valueType>
<valueListAllowed>true</valueListAllowed>
</parameter>
<parameter>
<name>ПараметрВремяСЗначением</name>
<valueType>
<v8:Type>xs:dateTime</v8:Type>
<v8:DateQualifiers>
<v8:DateFractions>Time</v8:DateFractions>
</v8:DateQualifiers>
</valueType>
<value xsi:type="xs:dateTime">0001-01-01T12:30:00</value>
</parameter>
<parameter>
<name>ПараметрВремяПусто</name>
<valueType>
<v8:Type>xs:dateTime</v8:Type>
<v8:DateQualifiers>
<v8:DateFractions>Time</v8:DateFractions>
</v8:DateQualifiers>
</valueType>
<value xsi:type="xs:dateTime">0001-01-01T00:00:00</value>
</parameter>
<parameter>
<name>ПараметрСтрокаФиксСЗначением</name>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>10</v8:Length>
<v8:AllowedLength>Fixed</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
<value xsi:type="xs:string">АБВ</value>
</parameter>
<parameter>
<name>ПараметрСтрокаФиксПусто</name>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>10</v8:Length>
<v8:AllowedLength>Fixed</v8:AllowedLength>
</v8:StringQualifiers>
</valueType>
<value xsi:type="xs:string"/>
</parameter>
<parameter>
<name>СоставнойТип</name>
<valueType>
<v8:Type>xs:string</v8:Type>
<v8:StringQualifiers>
<v8:Length>10</v8:Length>
<v8:AllowedLength>Fixed</v8:AllowedLength>
</v8:StringQualifiers>
<v8:Type xmlns:d5p1="http://v8.1c.ru/8.1/data/enterprise/current-config">d5p1:CatalogRef.ПлоскийПростой</v8:Type>
</valueType>
<value xsi:nil="true"/>
</parameter>
<settingsVariant>
<dcsset:name>Основной</dcsset:name>
<dcsset:presentation xsi:type="v8:LocalStringType">