diff --git a/matter/spec/commands.go b/matter/spec/commands.go index 9738e064..a21bc869 100644 --- a/matter/spec/commands.go +++ b/matter/spec/commands.go @@ -1,7 +1,7 @@ package spec import ( - "fmt" + "iter" "log/slog" "regexp" "strings" @@ -16,127 +16,62 @@ import ( var parentheticalExpressionPattern = regexp.MustCompile(`\s*\([^\)]+\)$`) -func (s *Section) toCommands(d *Doc, entityMap map[asciidoc.Attributable][]types.Entity) (commands matter.CommandSet, err error) { - - t := FindFirstTable(s) - if t == nil { - - return - } +type commandFactory struct{} - var commandMap map[string]*matter.Command - commands, commandMap, err = s.buildCommands(d, t) +func (cf *commandFactory) New(d *Doc, s *Section, row *asciidoc.TableRow, columnMap ColumnIndex, name string) (*matter.Command, error) { + cmd := matter.NewCommand(s.Base) + var err error + cmd.ID, err = readRowID(row, columnMap, matter.TableColumnID) if err != nil { - err = fmt.Errorf("error reading commands table: %w", err) - return + return nil, err } - for _, s := range parse.Skim[*Section](s.Elements()) { - switch s.SecType { - case matter.SectionCommand: - var c *matter.Command - c, err = s.toCommand(d, commandMap, entityMap) - if err != nil { - return - } - if c != nil { - entityMap[s.Base] = append(entityMap[s.Base], c) - } - } + cmd.Name = text.TrimCaseInsensitiveSuffix(name, " Command") + var dir string + dir, err = readRowASCIIDocString(row, columnMap, matter.TableColumnDirection) + if err != nil { + return nil, err } - return -} - -func (s *Section) buildCommands(d *Doc, t *asciidoc.Table) (commands matter.CommandSet, commandMap map[string]*matter.Command, err error) { - var rows []*asciidoc.TableRow - var headerRowIndex int - var columnMap ColumnIndex - rows, headerRowIndex, columnMap, _, err = parseTable(d, s, t) + cmd.Direction = ParseCommandDirection(dir) + cmd.Response, err = readRowASCIIDocString(row, columnMap, matter.TableColumnResponse) if err != nil { - return + return nil, err } - commandMap = make(map[string]*matter.Command) - for i := headerRowIndex + 1; i < len(rows); i++ { - row := rows[i] - cmd := matter.NewCommand(s.Base) - cmd.ID, err = readRowID(row, columnMap, matter.TableColumnID) - if err != nil { - return - } - cmd.Name, err = ReadRowValue(d, row, columnMap, matter.TableColumnName) - if err != nil { - return - } - cmd.Name = text.TrimCaseInsensitiveSuffix(cmd.Name, " Command") - var dir string - dir, err = readRowASCIIDocString(row, columnMap, matter.TableColumnDirection) - if err != nil { - return - } - cmd.Direction = ParseCommandDirection(dir) - cmd.Response, err = readRowASCIIDocString(row, columnMap, matter.TableColumnResponse) - if err != nil { - return - } - cmd.Conformance = d.getRowConformance(row, columnMap, matter.TableColumnConformance) - var a string - a, err = readRowASCIIDocString(row, columnMap, matter.TableColumnAccess) - if err != nil { - return - } - var q string - q, err = readRowASCIIDocString(row, columnMap, matter.TableColumnQuality) - if err != nil { - return - } - cmd.Quality = parseQuality(q, types.EntityTypeCommand, d, row) - cmd.Access, _ = ParseAccess(a, types.EntityTypeCommand) - commands = append(commands, cmd) - commandMap[strings.ToLower(cmd.Name)] = cmd + cmd.Conformance = d.getRowConformance(row, columnMap, matter.TableColumnConformance) + var a string + a, err = readRowASCIIDocString(row, columnMap, matter.TableColumnAccess) + if err != nil { + return nil, err } - - for _, cmd := range commands { - if cmd.Response != "" { - if responseCommand, ok := commandMap[strings.ToLower(cmd.Response)]; ok && responseCommand.Access.Invoke == matter.PrivilegeUnknown { - responseCommand.Access.Invoke = cmd.Access.Invoke - } - } + var q string + q, err = readRowASCIIDocString(row, columnMap, matter.TableColumnQuality) + if err != nil { + return nil, err } - return + cmd.Quality = parseQuality(q, types.EntityTypeCommand, d, row) + cmd.Access, _ = ParseAccess(a, types.EntityTypeCommand) + return cmd, nil } -func (s *Section) toCommand(d *Doc, commandMap map[string]*matter.Command, entityMap map[asciidoc.Attributable][]types.Entity) (*matter.Command, error) { - name := strings.ToLower(text.TrimCaseInsensitiveSuffix(s.Name, " Command")) - c, ok := commandMap[name] - if !ok { - // Command sometimes have an parenthetical abbreviation after their name - name = parentheticalExpressionPattern.ReplaceAllString(name, "") - c, ok = commandMap[name] - if !ok { - slog.Warn("unknown command", log.Element("path", d.Path, s.Base), "command", s.Name) - return nil, nil - } - } - +func (cf *commandFactory) Details(d *Doc, s *Section, entityMap map[asciidoc.Attributable][]types.Entity, c *matter.Command) (err error) { c.Description = getDescription(d, s.Elements()) var rows []*asciidoc.TableRow var headerRowIndex int var columnMap ColumnIndex - var err error rows, headerRowIndex, columnMap, _, err = parseFirstTable(d, s) if err != nil { if err == ErrNoTableFound { err = nil } else { - slog.Warn("No valid command parameter table found", log.Element("path", d.Path, s.Base), "command", name) + slog.Warn("No valid command parameter table found", log.Element("path", d.Path, s.Base), "command", c.Name) err = nil } - return nil, nil + return } c.Fields, err = d.readFields(headerRowIndex, rows, columnMap, types.EntityTypeCommandField) if err != nil { - return nil, err + return } fieldMap := make(map[string]*matter.Field, len(c.Fields)) for _, f := range c.Fields { @@ -144,10 +79,50 @@ func (s *Section) toCommand(d *Doc, commandMap map[string]*matter.Command, entit } err = s.mapFields(fieldMap, entityMap) if err != nil { - return nil, err + return } c.Name = CanonicalName(c.Name) - return c, nil + return +} + +func (cf *commandFactory) EntityName(s *Section) string { + name := strings.ToLower(text.TrimCaseInsensitiveSuffix(s.Name, " Command")) + return parentheticalExpressionPattern.ReplaceAllString(name, "") +} + +func (cf *commandFactory) Children(d *Doc, s *Section) iter.Seq[*Section] { + return func(yield func(*Section) bool) { + parse.SkimFunc(s.Elements(), func(s *Section) bool { + if s.SecType != matter.SectionCommand { + return false + } + return !yield(s) + }) + } +} + +func (s *Section) toCommands(d *Doc, entityMap map[asciidoc.Attributable][]types.Entity) (commands matter.CommandSet, err error) { + + t := FindFirstTable(s) + if t == nil { + return nil, nil + } + + var cf commandFactory + commands, err = buildList(d, s, t, entityMap, commands, &cf) + + for _, cmd := range commands { + if cmd.Response != "" { + for _, rc := range commands { + if strings.EqualFold(cmd.Response, rc.Name) { + rc.Access.Invoke = cmd.Access.Invoke + break + } + } + } + } + + return } func ParseCommandDirection(s string) matter.Interface { diff --git a/matter/spec/doc.go b/matter/spec/doc.go index dd17379a..f691e8b6 100644 --- a/matter/spec/doc.go +++ b/matter/spec/doc.go @@ -42,7 +42,7 @@ type Doc struct { errata *errata.Errata } -func NewDoc(d *asciidoc.Document, path Path) (*Doc, error) { +func newDoc(d *asciidoc.Document, path Path) (*Doc, error) { doc := &Doc{ Base: d, Path: path, @@ -192,7 +192,3 @@ func (doc *Doc) Reference(ref string) (types.Entity, bool) { } return entities[0], true } - -func GithubSettings() []asciidoc.AttributeName { - return []asciidoc.AttributeName{asciidoc.AttributeName("env-github")} -} diff --git a/matter/spec/event.go b/matter/spec/event.go index cb778ee6..465a3d74 100644 --- a/matter/spec/event.go +++ b/matter/spec/event.go @@ -2,7 +2,7 @@ package spec import ( "fmt" - "log/slog" + "iter" "strings" "github.com/project-chip/alchemy/asciidoc" @@ -12,11 +12,109 @@ import ( "github.com/project-chip/alchemy/matter/types" ) -func (s *Section) toEvents(d *Doc, entityMap map[asciidoc.Attributable][]types.Entity) (events matter.EventSet, err error) { +type eventFactory struct{} + +func (cf *eventFactory) New(d *Doc, s *Section, row *asciidoc.TableRow, columnMap ColumnIndex, name string) (e *matter.Event, err error) { + + e = matter.NewEvent(s.Base) + e.Name = matter.StripTypeSuffixes(name) + e.ID, err = readRowID(row, columnMap, matter.TableColumnID) + if err != nil { + return + } + e.Priority, err = readRowASCIIDocString(row, columnMap, matter.TableColumnPriority) + if err != nil { + return + } + e.Conformance = d.getRowConformance(row, columnMap, matter.TableColumnConformance) + var a string + a, err = readRowASCIIDocString(row, columnMap, matter.TableColumnAccess) + if err != nil { + return + } + e.Access, _ = ParseAccess(a, types.EntityTypeEvent) + if e.Access.Read == matter.PrivilegeUnknown { + // Sometimes the invoke access is omitted; we assume it's view + e.Access.Read = matter.PrivilegeView + } + e.Name = CanonicalName(e.Name) + return +} + +func (cf *eventFactory) Details(d *Doc, s *Section, entityMap map[asciidoc.Attributable][]types.Entity, e *matter.Event) (err error) { + e.Description = getDescription(d, s.Set) var rows []*asciidoc.TableRow var headerRowIndex int var columnMap ColumnIndex rows, headerRowIndex, columnMap, _, err = parseFirstTable(d, s) + if headerRowIndex > 0 { + firstRow := rows[0] + tableCells := firstRow.TableCells() + if len(tableCells) > 0 { + cv, rowErr := RenderTableCell(tableCells[0]) + if rowErr == nil { + cv = strings.ToLower(cv) + if strings.Contains(cv, "fabric sensitive") || strings.Contains(cv, "fabric-sensitive") { + e.Access.FabricSensitivity = matter.FabricSensitivitySensitive + } + } + } + } + if err != nil { + if err == ErrNoTableFound { + err = nil + return + } + err = fmt.Errorf("failed reading %s event fields: %w", s.Name, err) + return + } + e.Fields, err = d.readFields(headerRowIndex, rows, columnMap, types.EntityTypeEventField) + if err != nil { + return + } + entityMap[s.Base] = append(entityMap[s.Base], e) + fieldMap := make(map[string]*matter.Field, len(e.Fields)) + for _, f := range e.Fields { + fieldMap[f.Name] = f + } + err = s.mapFields(fieldMap, entityMap) + if err != nil { + return + } + for _, f := range e.Fields { + f.Name = CanonicalName(f.Name) + } + return +} + +func (cf *eventFactory) EntityName(s *Section) string { + return strings.ToLower(text.TrimCaseInsensitiveSuffix(s.Name, " Event")) +} + +func (cf *eventFactory) Children(d *Doc, s *Section) iter.Seq[*Section] { + return func(yield func(*Section) bool) { + parse.SkimFunc(s.Elements(), func(s *Section) bool { + if s.SecType != matter.SectionEvent { + return false + } + return !yield(s) + }) + } +} + +func (s *Section) toEvents(d *Doc, entityMap map[asciidoc.Attributable][]types.Entity) (events matter.EventSet, err error) { + t := FindFirstTable(s) + if t == nil { + return nil, nil + } + + var ef eventFactory + events, err = buildList(d, s, t, entityMap, events, &ef) + + /*var rows []*asciidoc.TableRow + var headerRowIndex int + var columnMap ColumnIndex + rows, headerRowIndex, columnMap, _, err = parseFirstTable(d, s) if err != nil { return nil, fmt.Errorf("failed reading events: %w", err) } @@ -65,49 +163,8 @@ func (s *Section) toEvents(d *Doc, entityMap map[asciidoc.Attributable][]types.E slog.Debug("unknown event", "event", name) continue } - e.Description = getDescription(d, s.Set) - var rows []*asciidoc.TableRow - var headerRowIndex int - var columnMap ColumnIndex - rows, headerRowIndex, columnMap, _, err = parseFirstTable(d, s) - if headerRowIndex > 0 { - firstRow := rows[0] - tableCells := firstRow.TableCells() - if len(tableCells) > 0 { - cv, rowErr := RenderTableCell(tableCells[0]) - if rowErr == nil { - cv = strings.ToLower(cv) - if strings.Contains(cv, "fabric sensitive") || strings.Contains(cv, "fabric-sensitive") { - e.Access.FabricSensitivity = matter.FabricSensitivitySensitive - } - } - } - } - if err != nil { - if err == ErrNoTableFound { - err = nil - continue - } - err = fmt.Errorf("failed reading %s event fields: %w", s.Name, err) - return - } - e.Fields, err = d.readFields(headerRowIndex, rows, columnMap, types.EntityTypeEventField) - if err != nil { - return - } - entityMap[s.Base] = append(entityMap[s.Base], e) - fieldMap := make(map[string]*matter.Field, len(e.Fields)) - for _, f := range e.Fields { - fieldMap[f.Name] = f - } - err = s.mapFields(fieldMap, entityMap) - if err != nil { - return - } - for _, f := range e.Fields { - f.Name = CanonicalName(f.Name) - } + } - } + }*/ return } diff --git a/matter/spec/global.go b/matter/spec/global.go index 44fcfd61..fb6adc99 100644 --- a/matter/spec/global.go +++ b/matter/spec/global.go @@ -1,6 +1,7 @@ package spec import ( + "iter" "log/slog" "github.com/project-chip/alchemy/asciidoc" @@ -67,6 +68,24 @@ func addGlobalEntities(spec *Specification, doc *Doc) error { return nil } +type globalCommandFactory struct { + commandFactory +} + +func (cf *globalCommandFactory) Children(d *Doc, s *Section) iter.Seq[*Section] { + return func(yield func(*Section) bool) { + parse.Traverse(d, d.Elements(), func(sec *Section, parent parse.HasElements, index int) parse.SearchShould { + if s.SecType != matter.SectionCommand { + return parse.SearchShouldContinue + } + if !yield(s) { + return parse.SearchShouldStop + } + return parse.SearchShouldContinue + }) + } +} + func (s *Section) toGlobalElements(d *Doc, entityMap map[asciidoc.Attributable][]types.Entity) (entities []types.Entity, err error) { var commandsTable *asciidoc.Table parse.SkimFunc(s.Elements(), func(t *asciidoc.Table) bool { @@ -84,8 +103,16 @@ func (s *Section) toGlobalElements(d *Doc, entityMap map[asciidoc.Attributable][ if commandsTable == nil { return } + + var cf globalCommandFactory var commands matter.CommandSet - var commandMap map[string]*matter.Command + commands, err = buildList(d, s, commandsTable, entityMap, commands, &cf) + + for _, c := range commands { + entities = append(entities, c) + } + + /*var commandMap map[string]*matter.Command commands, _, err = s.buildCommands(d, commandsTable) parse.Traverse(d, d.Elements(), func(sec *Section, parent parse.HasElements, index int) parse.SearchShould { switch s.SecType { @@ -103,7 +130,7 @@ func (s *Section) toGlobalElements(d *Doc, entityMap map[asciidoc.Attributable][ }) for _, c := range commands { entities = append(entities, c) - } + }*/ return } diff --git a/matter/spec/list.go b/matter/spec/list.go new file mode 100644 index 00000000..20eabc27 --- /dev/null +++ b/matter/spec/list.go @@ -0,0 +1,83 @@ +package spec + +import ( + "iter" + "log/slog" + "strings" + + "github.com/project-chip/alchemy/asciidoc" + "github.com/project-chip/alchemy/internal/log" + "github.com/project-chip/alchemy/matter" + "github.com/project-chip/alchemy/matter/types" +) + +type listIndex[T types.Entity] struct { + byName map[string]T + byReference map[asciidoc.Element]T +} + +type entityFactory[T types.Entity] interface { + New(d *Doc, s *Section, row *asciidoc.TableRow, columnMap ColumnIndex, name string) (T, error) + Details(d *Doc, s *Section, entityMap map[asciidoc.Attributable][]types.Entity, e T) error + EntityName(s *Section) string + Children(d *Doc, s *Section) iter.Seq[*Section] +} + +func buildList[T types.Entity, L ~[]T](d *Doc, s *Section, t *asciidoc.Table, entityMap map[asciidoc.Attributable][]types.Entity, list L, factory entityFactory[T]) (L, error) { + + index := listIndex[T]{ + byName: make(map[string]T), + byReference: make(map[asciidoc.Element]T), + } + var rows []*asciidoc.TableRow + var headerRowIndex int + var columnMap ColumnIndex + var err error + rows, headerRowIndex, columnMap, _, err = parseTable(d, s, t) + if err != nil { + return nil, err + } + for i := headerRowIndex + 1; i < len(rows); i++ { + row := rows[i] + + var name string + var xref *asciidoc.CrossReference + name, xref, err = readRowName(d, row, columnMap, matter.TableColumnName) + if err != nil { + return nil, err + } + + var entity T + entity, err = factory.New(d, s, row, columnMap, name) + if err != nil { + return nil, err + } + + list = append(list, entity) + index.byName[strings.ToLower(name)] = entity + if xref != nil { + anchor := d.FindAnchor(xref.ID) + if anchor != nil && anchor.Element != nil { + index.byReference[anchor.Element] = entity + } + } + } + + for s := range factory.Children(d, s) { + e, ok := index.byReference[s.Base] + if !ok { + name := factory.EntityName(s) + e, ok = index.byName[strings.ToLower(name)] + if !ok { + slog.Warn("unknown entity", log.Element("path", d.Path, s.Base), "entityName", s.Name) + continue + } + } + err = factory.Details(d, s, entityMap, e) + if err != nil { + return nil, err + } + entityMap[s.Base] = append(entityMap[s.Base], e) + } + return list, nil +} diff --git a/matter/spec/table.go b/matter/spec/table.go index b6c54f04..17bf19ea 100644 --- a/matter/spec/table.go +++ b/matter/spec/table.go @@ -76,6 +76,33 @@ func readRowID(row *asciidoc.TableRow, columnMap ColumnIndex, column matter.Tabl return matter.ParseNumber(id), nil } +func readRowName(doc *Doc, row *asciidoc.TableRow, columnMap ColumnIndex, columns ...matter.TableColumn) (name string, xref *asciidoc.CrossReference, err error) { + for _, column := range columns { + offset, ok := columnMap[column] + if !ok { + continue + } + cell := row.Cell(offset) + cellElements := cell.Elements() + for _, el := range cellElements { + switch el := el.(type) { + case *asciidoc.CrossReference: + xref = el + } + if xref != nil { + break + } + } + var value strings.Builder + err = readRowCellValueElements(doc, cellElements, &value) + if err != nil { + return "", nil, err + } + return strings.TrimSpace(value.String()), xref, nil + } + return "", nil, nil +} + func ReadRowValue(doc *Doc, row *asciidoc.TableRow, columnMap ColumnIndex, columns ...matter.TableColumn) (string, error) { for _, column := range columns { offset, ok := columnMap[column]