diff --git a/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs b/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs index bedb7ec..ac8bdae 100644 --- a/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs +++ b/graccesscli/src/ZB.MOM.WW.GRAccess.Cli/GRAccess/GRAccessCommandDispatcher.cs @@ -1417,23 +1417,86 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess return new { Supported = false, DataType = dataType, IsArray = false, Value = (object)null, Unavailable = "No supported scalar accessor succeeded for this MxValue." }; } + // Canonical script-text attribute suffixes a `ScriptExtension` primitive + // projects onto its parent object — see `docs/script-parsing.md` and + // `docs/usage.md:250`. The presence of any of these as `.` + // is the only attribute-side signal that the object carries a real + // ScriptExtension primitive (the GRAccess COM API does not expose an + // enumerable for primitives — only `AddExtensionPrimitive` / + // `DeleteExtensionPrimitive` / `RenameExtensionPrimitive` mutators). + private static readonly string[] ScriptTextSuffixes = + { + "ExecuteText", + "DeclarationsText", + "StartupText", + "ShutdownText", + "OnScanText", + "OffScanText", + "Expression", + }; + + // Returns the ScriptExtension prefix when `name` ends in one of the + // canonical script-text suffixes (e.g. `Foo.ExecuteText` -> `Foo`), + // else null. + private static string ScriptExtensionPrefix(string name) + { + if (string.IsNullOrEmpty(name)) return null; + var dot = name.LastIndexOf('.'); + if (dot <= 0 || dot == name.Length - 1) return null; + var suffix = name.Substring(dot + 1); + foreach (var s in ScriptTextSuffixes) + { + if (string.Equals(suffix, s, StringComparison.OrdinalIgnoreCase)) + return name.Substring(0, dot); + } + return null; + } + private static IEnumerable ObjectScripts(IgObject obj) { - var attributes = AttributeInfos(obj.Attributes) - .Where(IsScriptAttribute) - .GroupBy(a => a.Name, StringComparer.OrdinalIgnoreCase) - .Select(g => g.First()); + var attrs = AttributeInfos(obj.Attributes).ToList(); - foreach (var attr in attributes) + // Group attributes by their ScriptExtension prefix. Each + // ScriptExtension projects 1..N script-text attributes; one logical + // script entity per distinct prefix. Drops the previous + // `IsScriptAttribute` substring heuristic which false-matched + // `*.Description` (contains "script" in "deSCRIPTion") and + // `Scan*` attribute UDAs. + var byPrefix = attrs + .Select(a => new { Attr = a, Prefix = ScriptExtensionPrefix(a.Name) }) + .Where(x => x.Prefix != null) + .GroupBy(x => x.Prefix, StringComparer.OrdinalIgnoreCase); + + foreach (var grp in byPrefix) { + var prefix = grp.Key; + var fields = grp + .Select(x => x.Attr.Name.Substring(prefix.Length + 1)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) + .ToList(); + + // Sibling trigger metadata (the ScriptExtension also projects + // `.TriggerType` / `.TriggerPeriod` when set). + var triggerType = attrs.FirstOrDefault(a => + string.Equals(a.Name, prefix + ".TriggerType", StringComparison.OrdinalIgnoreCase))?.DataType; + var triggerPeriod = attrs.FirstOrDefault(a => + string.Equals(a.Name, prefix + ".TriggerPeriod", StringComparison.OrdinalIgnoreCase))?.DataType; + yield return new { - attr.Name, - attr.DataType, - attr.Category, + Name = prefix, + ExtensionType = "ScriptExtension", + Fields = fields, + TriggerType = triggerType, + TriggerPeriod = triggerPeriod, + // Carry empty DataType/Category so the outer ReadString-based + // wrapper still works without nulling out the JSON shape. + DataType = string.Empty, + Category = string.Empty, BodyAvailable = false, - Source = "attribute", - Unavailable = "Object script body access is version-specific and was not exposed through the generic GRAccess attribute metadata path." + Source = "attribute-prefix", + Unavailable = "Script body access requires the package fallback path." }; } } @@ -1448,16 +1511,34 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess foreach (var script in direct) { var name = ReadString(script, "Name"); + var fields = ReadProperty(script, "Fields") as IReadOnlyList + ?? (IReadOnlyList)((ReadProperty(script, "Fields") as IEnumerable)?.ToList()); emitted.Add(name); - var body = FindPackageScriptBody(package, name); + // Mark every `.` as emitted too so the + // package-only branch below doesn't double-yield each script + // text field as its own entry. + if (fields != null) + { + foreach (var f in fields) + emitted.Add(name + "." + f); + } + // Body lookup: try the prefix first, then the canonical body + // field `.ExecuteText` (the package's serialized + // script bodies are keyed by full attribute name). + var body = FindPackageScriptBody(package, name) + ?? FindPackageScriptBody(package, name + ".ExecuteText"); yield return new { Name = name, DataType = ReadString(script, "DataType"), Category = ReadString(script, "Category"), + ExtensionType = ReadString(script, "ExtensionType"), + Fields = fields, + TriggerType = ReadString(script, "TriggerType"), + TriggerPeriod = ReadString(script, "TriggerPeriod"), BodyAvailable = body != null, Body = body?.Body, - Source = body == null ? "attribute" : body.Source, + Source = body == null ? ReadString(script, "Source") : body.Source, PackageFallbackUsed = package.PackageFallbackUsed, Unavailable = body == null ? ReadString(script, "Unavailable") : string.Empty }; @@ -1470,6 +1551,10 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess body.Name, DataType = string.Empty, Category = string.Empty, + ExtensionType = "ScriptExtension", + Fields = (List)null, + TriggerType = (string)null, + TriggerPeriod = (string)null, BodyAvailable = true, Body = body.Body, Source = body.Source, @@ -2028,17 +2113,6 @@ namespace ZB.MOM.WW.GRAccess.Cli.GRAccess } } - private static bool IsScriptAttribute(GRAccessAttributeInfo attribute) - { - var name = attribute?.Name ?? string.Empty; - return name.IndexOf("script", StringComparison.OrdinalIgnoreCase) >= 0 - || name.IndexOf("expression", StringComparison.OrdinalIgnoreCase) >= 0 - || name.IndexOf("execute", StringComparison.OrdinalIgnoreCase) >= 0 - || name.IndexOf("startup", StringComparison.OrdinalIgnoreCase) >= 0 - || name.IndexOf("shutdown", StringComparison.OrdinalIgnoreCase) >= 0 - || name.IndexOf("scan", StringComparison.OrdinalIgnoreCase) >= 0; - } - private static MxValue CreateMxValue(string value, string dataType) { var mxValue = new MxValueClass();