diff --git a/soneta-programming/references/scan-modules.md b/soneta-programming/references/scan-modules.md index 3f2c225..41d8863 100644 --- a/soneta-programming/references/scan-modules.md +++ b/soneta-programming/references/scan-modules.md @@ -45,15 +45,25 @@ Algorytm: w l.mn. („Dokumenty handlowe"), bo opisują tabelę. Fallback: jeśli klasy `*Table` brak lub nie ma atrybutu, czytane są te same atrybuty z klasy `*Row`. Wartością jest pierwszy parametr `string` konstruktora atrybutu. - - `Guided` = `tak`, gdy klasa `*Table` dziedziczy (bezpośrednio lub pośrednio) z `GuidedTable` - albo `ExportedTable`. Tabele oznaczone `Guided=tak` są **rootami drzewa obiektów** — - stanowią korzeń paczki danych (`Datapack`/`GuidedRow`/`ExportedRow`) i to one są obsługiwane - przez mechanizm synchronizacji i eksportu/importu. Tabele bez tej flagi to elementy - szczegółowe (subrowy, info-rowy), które są częścią paczki danej tabeli-korzenia, ale nie - stanowią samodzielnego rootu. + - `Guided` — rozróżnia trzy stany: + - `root` — klasa `*Table` dziedziczy (bezpośrednio lub pośrednio) z `GuidedTable` + albo `ExportedTable`. Tabele te są **korzeniami drzewa obiektów** — stanowią root + paczki danych (`Datapack`/`GuidedRow`/`ExportedRow`) i to one są obsługiwane + przez mechanizm synchronizacji i eksportu/importu. + - `child: Pole→TypRow` — tabela jest częścią drzewa innego rootu; pole rekordu + z atrybutem `[ColumnInfo(GuidedRelation=…)]` wskazuje na tabelę nadrzędną. + `Pole` to nazwa pola w `*Record`, `TypRow` to konkretny typ `*Row` odczytany + z odpowiadającej property w klasie `*Row` (w `*Record` pole ma zwykle typ `IRow`). + - pusta wartość — tabela szczegółowa (subrow, info-row) niewchodząca w skład żadnego + drzewa guided. + - `Konfig` = `konfig`, gdy `*Table` ma `[TableInfo(IsConfig=true)]`. Tabele konfiguracyjne + żyją w osobnej sesji (`ExecuteConfig`) i mają inne reguły zapisu niż tabele operacyjne. + - `Interfaces` = lista nazw interfejsów zadeklarowanych w `[TableInfo(Interfaces = new[] { … })]`. + Soneta używa ich jako **relacji interfejsowych** — pole typu `IXxx` może referować rekord + z dowolnej tabeli deklarującej `IXxx` w swoim `TableInfo`. - Dla samego modułu (`*Module`) Tytuł/Opis czytane są analogicznie z atrybutów na klasie modułu. 6. Wypisz markdown: sekcja `##` per moduł (z jego `Caption`/`Description` jeśli są), w każdej - sekcji tabela `RowType | TableType | Tytuł | Opis`. + sekcji tabela `RowType | TableType | Guided | Konfig | Interfaces | Tytuł | Opis`. ## Wymagania @@ -89,14 +99,14 @@ Znaleziono modułów: 37 - Opis: Moduł handlowy obsługujący dokumenty sprzedaży, zakupu, zamówień i innych operacji handlowych... - Tabel: 62 -| RowType | TableType | Guided | Tytuł | Opis | -|---------|-----------|--------|-------|------| -| DefDokHandlowego | DefDokHandlowych | tak | Definicje dokumentów handlowych | Konfigurowalna definicja (szablon) dokumentu handlowego... | -| DefRelacjiHandlowej | DefRelHandlowych | tak | Definicje relacji handlowych | Konfigurowalna definicja relacji między dokumentami handlowymi... | -| DokumentHandlowy | DokHandlowe | tak | Dokumenty handlowe | Główna tabela dokumentów handlowych (faktury, paragony, zamówienia, korekty, umowy itp.)... | -| DokumentHandlowyKoszt | DokHandloweKoszt | | Koszty dodatkowe | Koszt dodatkowy przypisany do dokumentu handlowego... | -| DrukarkaFiskalna | DrukarkiFiskalne | tak | Lista drukarek fiskalnych | Konfiguracja drukarki fiskalnej... | -| ... | ... | ... | ... | ... | +| RowType | TableType | Guided | Konfig | Interfaces | Tytuł | Opis | +|---------|-----------|--------|--------|------------|-------|------| +| DefDokHandlowego | DefDokHandlowych | root | konfig | | Definicje dokumentów handlowych | Konfigurowalna definicja (szablon) dokumentu handlowego... | +| DefRelacjiHandlowej | DefRelHandlowych | root | konfig | | Definicje relacji handlowych | Konfigurowalna definicja relacji między dokumentami handlowymi... | +| DokumentHandlowy | DokHandlowe | root | | IDokument, IKontrahentRef | Dokumenty handlowe | Główna tabela dokumentów handlowych (faktury, paragony, zamówienia, korekty, umowy itp.)... | +| DokumentHandlowyKoszt | DokHandloweKoszt | child: Dokument→DokumentHandlowy | | | Koszty dodatkowe | Koszt dodatkowy przypisany do dokumentu handlowego... | +| DrukarkaFiskalna | DrukarkiFiskalne | root | konfig | | Lista drukarek fiskalnych | Konfiguracja drukarki fiskalnej... | +| ... | ... | ... | ... | ... | ... | ... | _Łącznie tabel: 1196_ ``` diff --git a/soneta-programming/references/scan-props.md b/soneta-programming/references/scan-props.md index d424779..6b58fbb 100644 --- a/soneta-programming/references/scan-props.md +++ b/soneta-programming/references/scan-props.md @@ -41,7 +41,24 @@ Algorytm: - znajdź klasę biznesową (`DefinicjaNumeracji`) oraz typ `*Module+DefinicjaNumeracjiRow` (mogą być w innym module — np. `CoreModule`); - powtórz całą procedurę (kroki 5–8) dla tego rekordu, używając prefiksu `Numeracja.` w kluczach wyników (`Numeracja.Pole1`, `Numeracja.Pole2`, …). Rekurencja działa dowolnie głęboko (subrow w subrowie). Pętle (rekord zawierający siebie pośrednio) są zabezpieczone przez zbiór odwiedzonych typów. -10. Wypisz tabelę markdown na stdout (kolumny: `Pole | Typ | Rodzaj | Tytuł | Opis`). +10. **Metadane tabeli** — dodatkowo do nagłówka trafiają: + - `Tabela konfiguracyjna: Tak/Nie` — czytane z `[TableInfo(IsConfig=true)]` na zagnieżdżonej + klasie `*Module.*Table` (atrybut siedzi tam, nie na top-levelowym typie zwracanym przez + property `Table` w `*Row`). + - `Guided: root` — gdy `*Table` dziedziczy z `GuidedTable`/`ExportedTable`. + - `Guided: child — nadrzędna przez pole \`X\` → \`Y\`` — gdy w rekordzie istnieje pole + z `[ColumnInfo(GuidedRelation=…)]` wskazujące tabelę nadrzędną w drzewie obiektów. + - `Implementuje interfejsy: …` — lista interfejsów z `[TableInfo(Interfaces=…)]` tej tabeli. +11. **Relacje interfejsowe** — skrypt buduje globalny indeks `interfejs → lista tabel implementujących` + (iteracja po wszystkich `*Module.*Table` we wszystkich referencjach). Dla każdego pola, którego + typ jest interfejsem występującym w tym indeksie (heurystyka: nazwa zaczyna się od `I` + wielka + litera), kolumna `Rodzaj` dostaje znacznik `iface-ref`, a po głównej tabeli pól wypisywana + jest sekcja `## Relacje interfejsowe` z listą `Pole | Interfejs | Tabele implementujące`. + Pozwala to od razu zobaczyć alternatywy, do których pole może wskazywać. +12. **Znacznik `guided-parent`** — pole rekordu z atrybutem `[ColumnInfo(GuidedRelation=…)]` + dostaje w kolumnie `Rodzaj` dodatkowy tag `guided-parent`, sygnalizując, że to ono trzyma + referencję do rootu drzewa. +13. Wypisz tabelę markdown na stdout (kolumny: `Pole | Typ | Rodzaj | Tytuł | Opis`). ## Wymagania @@ -70,6 +87,9 @@ dotnet script ~/.claude/skills/soneta-programming/scripts/scan-props.csx \ ```markdown # Pola i właściwości klasy biznesowej: `Soneta.Handel.DokumentHandlowy` Nazwa tabeli: `DokHandlowe` +Tabela konfiguracyjna: Nie +Guided: root +Implementuje interfejsy: `IDokument`, `IKontrahentRef` - pola bazodanowe: 128 - pola kalkulowane (z klas biznesowych): 388 @@ -78,14 +98,28 @@ Nazwa tabeli: `DokHandlowe` |------|-----|--------|-------|------| | Brutto | `decimal` | bazodanowe | Brutto | Wartość brutto dokumentu | | DataDokumentu | `System.DateTime` | bazodanowe | Data dokumentu | | -| Kontrahent | `Soneta.Kontrahenci.Kontrahent` | bazodanowe | Kontrahent | | +| Kontrahent | `Soneta.Kontrahenci.Kontrahent` | bazodanowe, iface-ref | Kontrahent | | | Netto | `decimal` | bazodanowe | Netto | | | Numer | `string` | bazodanowe | Numer | | | SaldoWaluta | `decimal` | | Saldo w walucie | | | ... | ... | ... | ... | ... | + +## Relacje interfejsowe + +Pola, których typ jest interfejsem zadeklarowanym w `[TableInfo(Interfaces=...)]` innych tabel. +Pole może wskazywać na rekord dowolnej z poniższych tabel. + +| Pole | Interfejs | Tabele implementujące | +|------|-----------|------------------------| +| Kontrahent | `IKontrahent` | `Kontrahent`, `Pracownik`, `Urzad` | ``` -Kolumna `Rodzaj` ma wartość `bazodanowe` dla pól rekordu lub jest pusta dla właściwości kalkulowanych. +Kolumna `Rodzaj` jest kombinacją znaczników rozdzielonych przecinkami: +- `bazodanowe` — pole rekordu (`*Record`); brak znacznika = property kalkulowana klasy biznesowej. +- `guided-parent` — pole z `[ColumnInfo(GuidedRelation=…)]` trzymające referencję do nadrzędnej + tabeli w drzewie obiektów guided. +- `iface-ref` — typ pola jest interfejsem zadeklarowanym w `[TableInfo(Interfaces=…)]` innej tabeli; + konkretne tabele docelowe są wymienione w sekcji `## Relacje interfejsowe` pod tabelą pól. ## Kody wyjścia diff --git a/soneta-programming/scripts/scan-modules.csx b/soneta-programming/scripts/scan-modules.csx index 5c1cbc6..1dde061 100644 --- a/soneta-programming/scripts/scan-modules.csx +++ b/soneta-programming/scripts/scan-modules.csx @@ -112,8 +112,8 @@ foreach (var module in modules) continue; } - Console.WriteLine("| RowType | TableType | Guided | Tytuł | Opis |"); - Console.WriteLine("|---------|-----------|--------|-------|------|"); + Console.WriteLine("| RowType | TableType | Guided | Konfig | Interfaces | Tytuł | Opis |"); + Console.WriteLine("|---------|-----------|--------|--------|------------|-------|------|"); foreach (var row in rowClasses) { var rowType = row.Name.EndsWith("Row") @@ -136,11 +136,19 @@ foreach (var module in modules) if (string.IsNullOrEmpty(description)) description = GetAttributeFirstString(row, "DescriptionAttribute"); - var guided = tableCls != null && InheritsFromGuidedOrExportedTable(tableCls) - ? "tak" - : ""; + var isGuidedRoot = tableCls != null && InheritsFromGuidedOrExportedTable(tableCls); + var guided = isGuidedRoot ? "root" : ""; + if (!isGuidedRoot) + { + var recordCls = module.GetTypeMembers(rowType + "Record").FirstOrDefault(); + var parent = FindGuidedParent(recordCls, row); + if (!string.IsNullOrEmpty(parent)) guided = "child: " + parent; + } - Console.WriteLine($"| {rowType} | {tableType} | {guided} | {EscapeCell(caption)} | {EscapeCell(description)} |"); + var konfig = IsConfigTable(tableCls) ? "konfig" : ""; + var interfaces = string.Join(", ", GetTableInterfaces(tableCls)); + + Console.WriteLine($"| {rowType} | {tableType} | {guided} | {konfig} | {EscapeCell(interfaces)} | {EscapeCell(caption)} | {EscapeCell(description)} |"); totalRows++; } Console.WriteLine(); @@ -185,6 +193,75 @@ static ISymbol FindMemberInherited(INamedTypeSymbol type, string name) return null; } +// Zwraca opis nadrzędnej tabeli w strukturze guided dla tabel guided-child. +// Pole rekordu oznaczone [ColumnInfo(GuidedRelation=RelationGuidedType.GuidedParent)] wskazuje +// kierunek relacji; konkretny typ Row pobieramy z property o tej samej nazwie w klasie *Row +// (w *Record pole ma zwykle typ IRow, więc bez Row nie da się ustalić konkretu). +static string FindGuidedParent(INamedTypeSymbol recordCls, INamedTypeSymbol rowCls) +{ + if (recordCls == null) return ""; + foreach (var f in recordCls.GetMembers().OfType()) + { + foreach (var a in f.GetAttributes()) + { + var an = a.AttributeClass?.Name; + if (an != "ColumnInfoAttribute" && an != "ColumnInfo") continue; + var hasGuided = a.NamedArguments.Any(na => na.Key == "GuidedRelation" + && na.Value.Kind == TypedConstantKind.Enum + && na.Value.Value is int v && v != 0); + if (!hasGuided) continue; + var propType = "?"; + if (rowCls != null) + { + for (var rc = rowCls; rc != null && rc.SpecialType != SpecialType.System_Object; rc = rc.BaseType) + { + var p = rc.GetMembers(f.Name).OfType().FirstOrDefault(); + if (p != null) { propType = p.Type.Name; break; } + } + } + return f.Name + "→" + propType; + } + } + return ""; +} + +// Lista interfejsów biznesowych z [TableInfo(Interfaces = new[] { "I1", "I2", ... })]. +// Soneta używa ich jako "relacji interfejsowych" — pole typu IXxx może referować dowolny +// rekord z tabeli, która deklaruje IXxx w swoim TableInfo. +static System.Collections.Generic.IEnumerable GetTableInterfaces(INamedTypeSymbol tableCls) +{ + if (tableCls == null) yield break; + foreach (var a in tableCls.GetAttributes()) + { + if (a.AttributeClass?.Name != "TableInfoAttribute" && a.AttributeClass?.Name != "TableInfo") + continue; + foreach (var na in a.NamedArguments) + { + if (na.Key != "Interfaces" || na.Value.Kind != TypedConstantKind.Array) continue; + foreach (var el in na.Value.Values) + { + if (el.Value is string s && !string.IsNullOrEmpty(s)) yield return s; + } + } + } +} + +static bool IsConfigTable(INamedTypeSymbol tableCls) +{ + if (tableCls == null) return false; + foreach (var a in tableCls.GetAttributes()) + { + if (a.AttributeClass?.Name != "TableInfoAttribute" && a.AttributeClass?.Name != "TableInfo") + continue; + foreach (var na in a.NamedArguments) + { + if (na.Key == "IsConfig" && na.Value.Value is bool b) + return b; + } + } + return false; +} + static string GetAttributeFirstString(ISymbol symbol, string attributeTypeName) { if (symbol == null) return ""; diff --git a/soneta-programming/scripts/scan-props.csx b/soneta-programming/scripts/scan-props.csx index 9efe41d..d1b517e 100644 --- a/soneta-programming/scripts/scan-props.csx +++ b/soneta-programming/scripts/scan-props.csx @@ -105,6 +105,7 @@ topLevelClasses.TryGetValue(recordBaseName, out mainBusinessClass); // Nazwa tabeli wyciągana z typu zwracanego przez property `Table` w klasie XxxxRow. string tableTypeName = null; +bool isConfigTable = false; var rowClass = enclosing?.GetTypeMembers(recordBaseName + "Row").FirstOrDefault(); if (rowClass != null) { @@ -118,6 +119,20 @@ if (rowClass != null) } } } +// Atrybut [TableInfo(IsConfig=true)] siedzi na klasie zagnieżdżonej XxxxModule.XxxxTable +// (nie na top-level typie tabeli zwracanym przez property `Table` w *Row). +var nestedTableCls = enclosing?.GetTypeMembers(recordBaseName + "Table").FirstOrDefault(); +if (nestedTableCls != null) + isConfigTable = IsConfigTable(nestedTableCls); + +// Wyznacz status guided: root (dziedziczy po GuidedTable/ExportedTable) lub child→ParentRow +// (pole rekordu z [ColumnInfo(GuidedRelation=...)]). Pole zapamiętujemy też w guidedParentField, +// żeby oznaczyć je później w tabeli pól. +var isGuidedRoot = nestedTableCls != null && InheritsFromGuidedOrExportedTable(nestedTableCls); +string guidedParentField = null; +string guidedParentType = null; +if (!isGuidedRoot) + (guidedParentField, guidedParentType) = FindGuidedParent(foundRecord, rowClass); // Klucz: nazwa pola z notacją kropkową dla subrowów; Wartość: (typ, czyBazodanowe, tytuł, opis) var merged = new SortedDictionary(StringComparer.Ordinal); @@ -138,6 +153,38 @@ else if (!string.IsNullOrEmpty(tableTypeName)) { Console.WriteLine($"Nazwa tabeli: `{tableTypeName}`"); + Console.WriteLine($"Tabela konfiguracyjna: {(isConfigTable ? "Tak" : "Nie")}"); + if (isGuidedRoot) + Console.WriteLine("Guided: root"); + else if (guidedParentField != null) + Console.WriteLine($"Guided: child — nadrzędna przez pole `{guidedParentField}` → `{guidedParentType}`"); + var thisInterfaces = nestedTableCls != null ? GetTableInterfaces(nestedTableCls).ToList() : new System.Collections.Generic.List(); + if (thisInterfaces.Count > 0) + Console.WriteLine($"Implementuje interfejsy: {string.Join(", ", thisInterfaces.Select(i => "`" + i + "`"))}"); +} + +// Indeks interfejs → lista tabel implementujących, na potrzeby pokazania alternatyw +// dla pól o typie interfejsowym (relacje interfejsowe Soneta). Klasy *Table są zagnieżdżone +// w *Module — iterujemy po top-level *Module i pobieramy ich nested types. +var interfaceImpls = new SortedDictionary>(StringComparer.Ordinal); +foreach (var asmRef in compilation.References) +{ + if (compilation.GetAssemblyOrModuleSymbol(asmRef) is not IAssemblySymbol asm) continue; + foreach (var top in EnumerateAllTypes(asm.GlobalNamespace)) + { + if (top.ContainingType != null || !top.Name.EndsWith("Module")) continue; + foreach (var t in top.GetTypeMembers()) + { + if (!t.Name.EndsWith("Table")) continue; + foreach (var iface in GetTableInterfaces(t)) + { + if (!interfaceImpls.TryGetValue(iface, out var list)) + interfaceImpls[iface] = list = new System.Collections.Generic.List(); + var rowName = t.Name.Substring(0, t.Name.Length - "Table".Length); + list.Add(rowName); + } + } + } } Console.WriteLine(); var dbCount = merged.Values.Count(v => v.IsDb); @@ -147,13 +194,48 @@ Console.WriteLine($"- pola kalkulowane (z klas biznesowych): {calcCount}"); Console.WriteLine(); Console.WriteLine("| Pole | Typ | Rodzaj | Tytuł | Opis |"); Console.WriteLine("|------|-----|--------|-------|------|"); +var interfaceFields = new System.Collections.Generic.List<(string Field, string IfaceShort, System.Collections.Generic.List Impls)>(); foreach (var kv in merged) { var rodzaj = kv.Value.IsDb ? "bazodanowe" : ""; + if (guidedParentField != null && kv.Key == guidedParentField) + rodzaj = string.IsNullOrEmpty(rodzaj) ? "guided-parent" : rodzaj + ", guided-parent"; + var shortType = ShortTypeName(kv.Value.Type); + if (shortType.StartsWith("I") && shortType.Length > 1 && char.IsUpper(shortType[1]) + && interfaceImpls.TryGetValue(shortType, out var impls)) + { + rodzaj = string.IsNullOrEmpty(rodzaj) ? "iface-ref" : rodzaj + ", iface-ref"; + interfaceFields.Add((kv.Key, shortType, impls)); + } Console.WriteLine($"| {kv.Key} | `{kv.Value.Type}` | {rodzaj} | {EscapeCell(kv.Value.Caption)} | {EscapeCell(kv.Value.Description)} |"); } + +if (interfaceFields.Count > 0) +{ + Console.WriteLine(); + Console.WriteLine("## Relacje interfejsowe"); + Console.WriteLine(); + Console.WriteLine("Pola, których typ jest interfejsem zadeklarowanym w `[TableInfo(Interfaces=...)]` innych tabel."); + Console.WriteLine("Pole może wskazywać na rekord dowolnej z poniższych tabel."); + Console.WriteLine(); + Console.WriteLine("| Pole | Interfejs | Tabele implementujące |"); + Console.WriteLine("|------|-----------|------------------------|"); + foreach (var f in interfaceFields) + { + Console.WriteLine($"| {f.Field} | `{f.IfaceShort}` | {string.Join(", ", f.Impls.Select(i => "`" + i + "`"))} |"); + } +} return 0; +static string ShortTypeName(string fullName) +{ + if (string.IsNullOrEmpty(fullName)) return ""; + var lt = fullName.IndexOf('<'); + if (lt >= 0) fullName = fullName.Substring(0, lt); + var dot = fullName.LastIndexOf('.'); + return dot >= 0 ? fullName.Substring(dot + 1) : fullName; +} + static void ScanRecord( INamedTypeSymbol record, string prefix, @@ -267,6 +349,77 @@ static IEnumerable EnumerateInheritedProperties(INamedTypeSymbo } } +static bool InheritsFromGuidedOrExportedTable(INamedTypeSymbol type) +{ + for (var t = type.BaseType; t != null && t.SpecialType != SpecialType.System_Object; t = t.BaseType) + { + if (t.Name == "GuidedTable" || t.Name == "ExportedTable") return true; + } + return false; +} + +static (string field, string parentType) FindGuidedParent(INamedTypeSymbol recordCls, INamedTypeSymbol rowCls) +{ + if (recordCls == null) return (null, null); + foreach (var f in recordCls.GetMembers().OfType()) + { + foreach (var a in f.GetAttributes()) + { + var an = a.AttributeClass?.Name; + if (an != "ColumnInfoAttribute" && an != "ColumnInfo") continue; + var hasGuided = a.NamedArguments.Any(na => na.Key == "GuidedRelation" + && na.Value.Kind == TypedConstantKind.Enum + && na.Value.Value is int v && v != 0); + if (!hasGuided) continue; + var propType = "?"; + if (rowCls != null) + { + for (var rc = rowCls; rc != null && rc.SpecialType != SpecialType.System_Object; rc = rc.BaseType) + { + var p = rc.GetMembers(f.Name).OfType().FirstOrDefault(); + if (p != null) { propType = p.Type.Name; break; } + } + } + return (f.Name, propType); + } + } + return (null, null); +} + +static System.Collections.Generic.IEnumerable GetTableInterfaces(INamedTypeSymbol tableCls) +{ + if (tableCls == null) yield break; + foreach (var a in tableCls.GetAttributes()) + { + if (a.AttributeClass?.Name != "TableInfoAttribute" && a.AttributeClass?.Name != "TableInfo") + continue; + foreach (var na in a.NamedArguments) + { + if (na.Key != "Interfaces" || na.Value.Kind != TypedConstantKind.Array) continue; + foreach (var el in na.Value.Values) + { + if (el.Value is string s && !string.IsNullOrEmpty(s)) yield return s; + } + } + } +} + +static bool IsConfigTable(INamedTypeSymbol tableCls) +{ + if (tableCls == null) return false; + foreach (var a in tableCls.GetAttributes()) + { + if (a.AttributeClass?.Name != "TableInfoAttribute" && a.AttributeClass?.Name != "TableInfo") + continue; + foreach (var na in a.NamedArguments) + { + if (na.Key == "IsConfig" && na.Value.Value is bool b) + return b; + } + } + return false; +} + static string GetAttributeFirstString(ISymbol symbol, string attributeTypeName) { if (symbol == null) return "";