- assets/: sap-cli source (v2.1.0, 26 commands, 16 object types) - references/: tool constraints + error handling (self-contained) - scripts/setup.py: one-click install/config/verify - SKILL.md: full command reference + dual-platform install guide - VERSION: 2.1.0 Built from D:/Codespace/sap-cli via scripts/pack_skill.py
278 lines
10 KiB
Python
278 lines
10 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import dataclass, field
|
|
|
|
from sapcli.utils.xml_utils import ddl_escape as _ddl_escape
|
|
from sapcli.utils.xml_utils import xml_escape as _xml_escape
|
|
|
|
|
|
@dataclass
|
|
class DomainDefinition:
|
|
datatype: str = "CHAR"
|
|
length: int = 10
|
|
decimals: int = 0
|
|
lowercase: bool = False
|
|
fix_values: list[dict[str, str]] = field(default_factory=list)
|
|
|
|
def to_xml(self, name: str, description: str, package: str = "$TMP") -> str:
|
|
ns = (
|
|
'xmlns:doma="http://www.sap.com/dictionary/domain"'
|
|
' xmlns:adtcore="http://www.sap.com/adt/core"'
|
|
)
|
|
header = (
|
|
f'<doma:domain {ns}'
|
|
f' adtcore:name="{_xml_escape(name)}"'
|
|
f' adtcore:type="DOMA/DD"'
|
|
f' adtcore:description="{_xml_escape(description)}"'
|
|
f' adtcore:language="EN">'
|
|
f'<adtcore:packageRef adtcore:name="{_xml_escape(package)}"/>'
|
|
)
|
|
type_info = (
|
|
"<doma:content>"
|
|
"<doma:typeInformation>"
|
|
f"<doma:datatype>{self.datatype}</doma:datatype>"
|
|
f"<doma:length>{self.length:06d}</doma:length>"
|
|
f"<doma:decimals>{self.decimals:06d}</doma:decimals>"
|
|
"</doma:typeInformation>"
|
|
"<doma:outputInformation>"
|
|
f"<doma:length>{self.length:06d}</doma:length>"
|
|
"<doma:style>00</doma:style>"
|
|
"<doma:conversionExit/>"
|
|
f"<doma:signExists>false</doma:signExists>"
|
|
f"<doma:lowercase>{'true' if self.lowercase else 'false'}</doma:lowercase>"
|
|
"<doma:ampmFormat>false</doma:ampmFormat>"
|
|
"</doma:outputInformation>"
|
|
)
|
|
fix_xml = ""
|
|
if self.fix_values:
|
|
fix_xml = "<doma:valueInformation><doma:valueTableRef/><doma:appendExists>false</doma:appendExists><doma:fixValues>"
|
|
for i, fv in enumerate(self.fix_values, 1):
|
|
low = fv.get("low", "")
|
|
high = fv.get("high", "")
|
|
text = fv.get("text", "")
|
|
fix_xml += (
|
|
f"<doma:fixValue>"
|
|
f"<doma:position>{i:04d}</doma:position>"
|
|
f"<doma:low>{low}</doma:low>"
|
|
f"<doma:high>{high}</doma:high>"
|
|
f"<doma:text>{text}</doma:text>"
|
|
f"</doma:fixValue>"
|
|
)
|
|
fix_xml += "</doma:fixValues></doma:valueInformation>"
|
|
|
|
return f"<?xml version=\"1.0\" encoding=\"UTF-8\"?>{header}{type_info}{fix_xml}</doma:content></doma:domain>"
|
|
|
|
|
|
@dataclass
|
|
class DataElementDefinition:
|
|
datatype: str = "CHAR"
|
|
length: int = 10
|
|
decimals: int = 0
|
|
domain_name: str = ""
|
|
|
|
def to_xml(self, name: str, description: str, package: str = "$TMP") -> str:
|
|
ns = (
|
|
'xmlns:blue="http://www.sap.com/wbobj/dictionary/dtel"'
|
|
' xmlns:adtcore="http://www.sap.com/adt/core"'
|
|
)
|
|
header = (
|
|
f'<blue:wbobj {ns}'
|
|
f' adtcore:name="{_xml_escape(name)}"'
|
|
f' adtcore:type="DTEL/DE"'
|
|
f' adtcore:description="{_xml_escape(description)}"'
|
|
f' adtcore:language="EN">'
|
|
f'<adtcore:packageRef adtcore:name="{_xml_escape(package)}"/>'
|
|
)
|
|
type_block = (
|
|
'<dtel:dataElement xmlns:dtel="http://www.sap.com/adt/dictionary/dataelements">'
|
|
)
|
|
if self.domain_name:
|
|
type_block += (
|
|
f"<dtel:typeKind>domain</dtel:typeKind>"
|
|
f"<dtel:typeName>{self.domain_name}</dtel:typeName>"
|
|
"<dtel:dataType/>"
|
|
"<dtel:dataTypeLength/>"
|
|
"<dtel:dataTypeDecimals/>"
|
|
)
|
|
else:
|
|
type_block += (
|
|
"<dtel:typeKind>predefinedAbapType</dtel:typeKind>"
|
|
"<dtel:typeName/>"
|
|
f"<dtel:dataType>{self.datatype}</dtel:dataType>"
|
|
f"<dtel:dataTypeLength>{self.length:06d}</dtel:dataTypeLength>"
|
|
f"<dtel:dataTypeDecimals>{self.decimals:06d}</dtel:dataTypeDecimals>"
|
|
)
|
|
type_block += (
|
|
"<dtel:shortFieldLabel/>"
|
|
"<dtel:shortFieldLength>00</dtel:shortFieldLength>"
|
|
"<dtel:shortFieldMaxLength>10</dtel:shortFieldMaxLength>"
|
|
"<dtel:mediumFieldLabel/>"
|
|
"<dtel:mediumFieldLength>00</dtel:mediumFieldLength>"
|
|
"<dtel:mediumFieldMaxLength>20</dtel:mediumFieldMaxLength>"
|
|
"<dtel:longFieldLabel/>"
|
|
"<dtel:longFieldLength>00</dtel:longFieldLength>"
|
|
"<dtel:longFieldMaxLength>40</dtel:longFieldMaxLength>"
|
|
"<dtel:headingFieldLabel/>"
|
|
"<dtel:headingFieldLength>00</dtel:headingFieldLength>"
|
|
"<dtel:headingFieldMaxLength>55</dtel:headingFieldMaxLength>"
|
|
"<dtel:searchHelp/>"
|
|
"<dtel:searchHelpParameter/>"
|
|
"<dtel:setGetParameter/>"
|
|
"<dtel:defaultComponentName/>"
|
|
"<dtel:deactivateInputHistory>false</dtel:deactivateInputHistory>"
|
|
"<dtel:changeDocument>false</dtel:changeDocument>"
|
|
"<dtel:leftToRightDirection>false</dtel:leftToRightDirection>"
|
|
"<dtel:deactivateBIDIFiltering>false</dtel:deactivateBIDIFiltering>"
|
|
"</dtel:dataElement>"
|
|
)
|
|
return f"<?xml version=\"1.0\" encoding=\"UTF-8\"?>{header}{type_block}</blue:wbobj>"
|
|
|
|
|
|
@dataclass
|
|
class TableField:
|
|
name: str
|
|
type_name: str
|
|
is_key: bool = False
|
|
not_null: bool = False
|
|
|
|
def to_ddl(self) -> str:
|
|
parts = [f" {self.name:40s}: {self.type_name}"]
|
|
if self.not_null:
|
|
parts.append("not null")
|
|
return " ".join(parts) + ";"
|
|
|
|
|
|
@dataclass
|
|
class TableDefinition:
|
|
fields: list[TableField] = field(default_factory=list)
|
|
label: str = ""
|
|
enhancement_category: str = "#NOT_CLASSIFIED"
|
|
delivery_class: str = "#A"
|
|
data_maintenance: str = "#LIMITED"
|
|
table_category: str = "#TRANSPARENT"
|
|
|
|
def to_ddl(self, name: str, description: str) -> str:
|
|
lines = []
|
|
lines.append(f"@EndUserText.label : '{_ddl_escape(description)}'")
|
|
lines.append(f"@AbapCatalog.enhancementCategory : {self.enhancement_category}")
|
|
lines.append(f"@AbapCatalog.tableCategory : {self.table_category}")
|
|
lines.append(f"@AbapCatalog.deliveryClass : {self.delivery_class}")
|
|
lines.append(f"@AbapCatalog.dataMaintenance : {self.data_maintenance}")
|
|
lines.append(f"define table {name.lower()} {{")
|
|
for f in self.fields:
|
|
lines.append(f.to_ddl())
|
|
lines.append("}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
@dataclass
|
|
class StructureDefinition:
|
|
fields: list[TableField] = field(default_factory=list)
|
|
enhancement_category: str = "#NOT_CLASSIFIED"
|
|
|
|
def to_ddl(self, name: str, description: str) -> str:
|
|
lines = []
|
|
lines.append(f"@EndUserText.label : '{_ddl_escape(description)}'")
|
|
lines.append(f"@AbapCatalog.enhancementCategory : {self.enhancement_category}")
|
|
lines.append(f"define structure {name.lower()} {{")
|
|
for f in self.fields:
|
|
lines.append(f.to_ddl())
|
|
lines.append("}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
@dataclass
|
|
class TableTypeDefinition:
|
|
line_type: str = ""
|
|
key_type: str = "#USER_DEFINED"
|
|
access_mode: str = "#STANDARD"
|
|
key_fields: list[str] = field(default_factory=list)
|
|
|
|
def to_xml(self, name: str, description: str, package: str = "$TMP") -> str:
|
|
ns = (
|
|
'xmlns:ttyp="http://www.sap.com/adt/dictionary/tabletypes"'
|
|
' xmlns:adtcore="http://www.sap.com/adt/core"'
|
|
)
|
|
header = (
|
|
f'<ttyp:tableType {ns}'
|
|
f' adtcore:name="{_xml_escape(name)}"'
|
|
f' adtcore:type="TTYP"'
|
|
f' adtcore:description="{_xml_escape(description)}"'
|
|
f' adtcore:language="EN">'
|
|
f'<adtcore:packageRef adtcore:name="{_xml_escape(package)}"/>'
|
|
)
|
|
body = (
|
|
"<ttyp:content>"
|
|
"<ttyp:accessMode>"
|
|
f"<ttyp:mode>{self.access_mode}</ttyp:mode>"
|
|
"</ttyp:accessMode>"
|
|
"<ttyp:keyDefinition>"
|
|
f"<ttyp:definition>{self.key_type}</ttyp:definition>"
|
|
"</ttyp:keyDefinition>"
|
|
"<ttyp:rowType>"
|
|
f"<ttyp:typeKind>structured</ttyp:typeKind>"
|
|
f"<ttyp:typeName>{self.line_type}</ttyp:typeName>"
|
|
"</ttyp:rowType>"
|
|
"</ttyp:content>"
|
|
)
|
|
return f"<?xml version=\"1.0\" encoding=\"UTF-8\"?>{header}{body}</ttyp:tableType>"
|
|
|
|
|
|
def load_definition_from_json(path: str, obj_type: str) -> str | None:
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
return build_definition(data, obj_type)
|
|
|
|
|
|
def build_definition(data: dict, obj_type: str) -> str | None:
|
|
if obj_type == "domain":
|
|
return _build_domain(data)
|
|
elif obj_type == "dataelement":
|
|
return _build_dataelement(data)
|
|
elif obj_type in ("table", "structure"):
|
|
return None
|
|
elif obj_type == "tabletype":
|
|
return _build_tabletype(data)
|
|
return None
|
|
|
|
|
|
def _build_domain(data: dict) -> str:
|
|
defn = DomainDefinition(
|
|
datatype=data.get("datatype", "CHAR"),
|
|
length=data.get("length", 10),
|
|
decimals=data.get("decimals", 0),
|
|
lowercase=data.get("lowercase", False),
|
|
fix_values=data.get("fix_values", []),
|
|
)
|
|
name = data.get("name", "ZDOMAIN")
|
|
description = data.get("description", name)
|
|
package = data.get("package", "$TMP")
|
|
return defn.to_xml(name, description, package)
|
|
|
|
|
|
def _build_dataelement(data: dict) -> str:
|
|
defn = DataElementDefinition(
|
|
datatype=data.get("datatype", "CHAR"),
|
|
length=data.get("length", 10),
|
|
decimals=data.get("decimals", 0),
|
|
domain_name=data.get("domain_name", ""),
|
|
)
|
|
name = data.get("name", "ZDATAELEMENT")
|
|
description = data.get("description", name)
|
|
package = data.get("package", "$TMP")
|
|
return defn.to_xml(name, description, package)
|
|
|
|
|
|
def _build_tabletype(data: dict) -> str:
|
|
defn = TableTypeDefinition(
|
|
line_type=data.get("line_type", ""),
|
|
key_type=data.get("key_type", "#USER_DEFINED"),
|
|
access_mode=data.get("access_mode", "#STANDARD"),
|
|
key_fields=data.get("key_fields", []),
|
|
)
|
|
name = data.get("name", "ZTABLETYPE")
|
|
description = data.get("description", name)
|
|
package = data.get("package", "$TMP")
|
|
return defn.to_xml(name, description, package)
|