From 2113e6fe3d32e381744f7cb4155111fa2e7c793b Mon Sep 17 00:00:00 2001 From: Hasty Granbery Date: Tue, 1 Oct 2024 10:42:18 -0700 Subject: [PATCH] Some DRYing up of code base --- disco/constraint.go | 2 +- matter/cluster.go | 90 +------ matter/datatypes.go | 65 ++++- matter/spec/attributes.go | 2 +- matter/spec/build.go | 235 ++++------------- matter/spec/cluster.go | 3 +- matter/spec/commands.go | 2 +- matter/spec/datatypes.go | 266 +++++++++---------- matter/spec/field.go | 86 ++++++ matter/spec/table.go | 375 -------------------------- matter/spec/table_info.go | 540 ++++++++++++++++++++++++++++++++++++++ matter/typedef.go | 2 + 12 files changed, 878 insertions(+), 790 deletions(-) create mode 100644 matter/spec/table_info.go diff --git a/disco/constraint.go b/disco/constraint.go index 5159afac..856a4371 100644 --- a/disco/constraint.go +++ b/disco/constraint.go @@ -29,7 +29,7 @@ func (b *Ball) fixConstraintCells(section *spec.Section, ti *spec.TableInfo) (er continue } - dataType, e := b.doc.ReadRowDataType(row, ti.ColumnMap, matter.TableColumnType) + dataType, e := ti.ReadDataType(row, matter.TableColumnType) if e != nil { slog.Debug("error reading data type for constraint", slog.String("path", b.doc.Path.String()), slog.Any("error", e)) continue diff --git a/matter/cluster.go b/matter/cluster.go index 318b6e3b..4f6e9892 100644 --- a/matter/cluster.go +++ b/matter/cluster.go @@ -1,10 +1,7 @@ package matter import ( - "log/slog" - "github.com/project-chip/alchemy/asciidoc" - "github.com/project-chip/alchemy/internal/log" "github.com/project-chip/alchemy/matter/conformance" "github.com/project-chip/alchemy/matter/types" ) @@ -17,11 +14,13 @@ type ClusterGroup struct { } func NewClusterGroup(name string, source asciidoc.Element, clusters []*Cluster) *ClusterGroup { - return &ClusterGroup{ + cg := &ClusterGroup{ Name: name, entity: entity{source: source}, Clusters: clusters, } + cg.AssociatedDataTypes.parentEntity = cg + return cg } func (c ClusterGroup) EntityType() types.EntityType { @@ -32,45 +31,6 @@ func (c ClusterGroup) Explode() []*Cluster { return c.Clusters } -func (c *ClusterGroup) AddBitmaps(bitmaps ...*Bitmap) { - for _, bm := range bitmaps { - if bm.ParentEntity != nil { - if _, ok := bm.ParentEntity.(*ClusterGroup); !ok { - slog.Warn("Bitmap belongs to multiple clusters", slog.String("name", bm.Name), log.Path("source", bm)) - } - continue - } - bm.ParentEntity = c - } - c.Bitmaps = append(c.Bitmaps, bitmaps...) -} - -func (c *ClusterGroup) AddEnums(enums ...*Enum) { - for _, e := range enums { - if e.ParentEntity != nil { - if _, ok := e.ParentEntity.(*ClusterGroup); !ok { - slog.Warn("Enum belongs to multiple clusters", slog.String("name", e.Name), log.Path("source", e)) - } - continue - } - e.ParentEntity = c - } - c.Enums = append(c.Enums, enums...) -} - -func (c *ClusterGroup) AddStructs(structs ...*Struct) { - for _, s := range structs { - if s.ParentEntity != nil { - if _, ok := s.ParentEntity.(*ClusterGroup); !ok { - slog.Warn("Struct belongs to multiple clusters", slog.String("name", s.Name), log.Path("source", s)) - } - continue - } - s.ParentEntity = c - } - c.Structs = append(c.Structs, structs...) -} - type Cluster struct { entity ID *Number `json:"id,omitempty"` @@ -87,16 +47,17 @@ type Cluster struct { Features *Features `json:"features,omitempty"` AssociatedDataTypes - TypeDefs TypeDefSet `json:"typedefs,omitempty"` Attributes FieldSet `json:"attributes,omitempty"` Events EventSet `json:"events,omitempty"` Commands CommandSet `json:"commands,omitempty"` } func NewCluster(source asciidoc.Element) *Cluster { - return &Cluster{ + c := &Cluster{ entity: entity{source: source}, } + c.AssociatedDataTypes.parentEntity = c + return c } func (c *Cluster) EntityType() types.EntityType { @@ -231,42 +192,3 @@ func (c *Cluster) Identifier(name string) (types.Entity, bool) { } return nil, false } - -func (c *Cluster) AddBitmaps(bitmaps ...*Bitmap) { - for _, bm := range bitmaps { - if bm.ParentEntity != nil { - if _, ok := bm.ParentEntity.(*ClusterGroup); !ok { - slog.Warn("Bitmap belongs to multiple clusters", slog.String("name", bm.Name), log.Path("source", bm), slog.String("cluster", c.Name)) - } - continue - } - bm.ParentEntity = c - } - c.Bitmaps = append(c.Bitmaps, bitmaps...) -} - -func (c *Cluster) AddEnums(enums ...*Enum) { - for _, e := range enums { - if e.ParentEntity != nil { - if _, ok := e.ParentEntity.(*ClusterGroup); !ok { - slog.Warn("Enum belongs to multiple clusters", slog.String("name", e.Name), log.Path("source", e)) - } - continue - } - e.ParentEntity = c - } - c.Enums = append(c.Enums, enums...) -} - -func (c *Cluster) AddStructs(structs ...*Struct) { - for _, s := range structs { - if s.ParentEntity != nil { - if _, ok := s.ParentEntity.(*ClusterGroup); !ok { - slog.Warn("Struct belongs to multiple clusters", slog.String("name", s.Name), log.Path("source", s), slog.String("cluster", c.Name)) - } - continue - } - s.ParentEntity = c - } - c.Structs = append(c.Structs, structs...) -} diff --git a/matter/datatypes.go b/matter/datatypes.go index 355ba213..30a2cbf5 100644 --- a/matter/datatypes.go +++ b/matter/datatypes.go @@ -1,7 +1,11 @@ package matter import ( + "log/slog" "strings" + + "github.com/project-chip/alchemy/internal/log" + "github.com/project-chip/alchemy/matter/types" ) type DataTypeCategory uint8 @@ -54,7 +58,62 @@ func StripTypeSuffixes(dataType string) string { } type AssociatedDataTypes struct { - Bitmaps BitmapSet `json:"bitmaps,omitempty"` - Enums EnumSet `json:"enums,omitempty"` - Structs StructSet `json:"structs,omitempty"` + parentEntity types.Entity + + Bitmaps BitmapSet `json:"bitmaps,omitempty"` + Enums EnumSet `json:"enums,omitempty"` + Structs StructSet `json:"structs,omitempty"` + TypeDefs TypeDefSet `json:"typedefs,omitempty"` +} + +func (adt *AssociatedDataTypes) AddBitmaps(bitmaps ...*Bitmap) { + for _, bm := range bitmaps { + if bm.ParentEntity != nil { + if _, ok := bm.ParentEntity.(*ClusterGroup); !ok { + slog.Warn("Bitmap belongs to multiple parents", slog.String("name", bm.Name), log.Path("source", bm), LogEntity(adt.parentEntity)) + } + continue + } + bm.ParentEntity = adt.parentEntity + } + adt.Bitmaps = append(adt.Bitmaps, bitmaps...) +} + +func (adt *AssociatedDataTypes) AddEnums(enums ...*Enum) { + for _, e := range enums { + if e.ParentEntity != nil { + if _, ok := e.ParentEntity.(*ClusterGroup); !ok { + slog.Warn("Enum belongs to multiple parents", slog.String("name", e.Name), log.Path("source", e), LogEntity(adt.parentEntity)) + } + continue + } + e.ParentEntity = adt.parentEntity + } + adt.Enums = append(adt.Enums, enums...) +} + +func (adt *AssociatedDataTypes) AddStructs(structs ...*Struct) { + for _, s := range structs { + if s.ParentEntity != nil { + if _, ok := s.ParentEntity.(*ClusterGroup); !ok { + slog.Warn("Struct belongs to multiple parents", slog.String("name", s.Name), log.Path("source", s), LogEntity(adt.parentEntity)) + } + continue + } + s.ParentEntity = adt.parentEntity + } + adt.Structs = append(adt.Structs, structs...) +} + +func (adt *AssociatedDataTypes) AddTypeDefs(typeDefs ...*TypeDef) { + for _, td := range typeDefs { + if td.ParentEntity != nil { + if _, ok := td.ParentEntity.(*ClusterGroup); !ok { + slog.Warn("TypeDef belongs to multiple parents", slog.String("name", td.Name), log.Path("source", td), LogEntity(adt.parentEntity)) + } + continue + } + td.ParentEntity = adt.parentEntity + } + adt.TypeDefs = append(adt.TypeDefs, typeDefs...) } diff --git a/matter/spec/attributes.go b/matter/spec/attributes.go index 78895766..e3aa9ff5 100644 --- a/matter/spec/attributes.go +++ b/matter/spec/attributes.go @@ -32,7 +32,7 @@ func (s *Section) toAttributes(d *Doc, cluster *matter.Cluster, entityMap map[as } attr.Name = matter.StripTypeSuffixes(attr.Name) attr.Conformance = ti.ReadConformance(row, matter.TableColumnConformance) - attr.Type, err = d.ReadRowDataType(row, ti.ColumnMap, matter.TableColumnType) + attr.Type, err = ti.ReadDataType(row, matter.TableColumnType) if err != nil { if cluster.Hierarchy == "Base" && !conformance.IsDeprecated(attr.Conformance) && !conformance.IsDisallowed(attr.Conformance) { // Clusters inheriting from other clusters don't supply type information, nor do attributes that are deprecated or disallowed diff --git a/matter/spec/build.go b/matter/spec/build.go index 02a40504..91cc85eb 100644 --- a/matter/spec/build.go +++ b/matter/spec/build.go @@ -6,11 +6,9 @@ import ( "log/slog" "strings" - "github.com/project-chip/alchemy/asciidoc" "github.com/project-chip/alchemy/internal/log" "github.com/project-chip/alchemy/internal/pipeline" "github.com/project-chip/alchemy/matter" - "github.com/project-chip/alchemy/matter/conformance" "github.com/project-chip/alchemy/matter/types" ) @@ -48,34 +46,13 @@ func (sp *Builder) buildSpec(docs []*Doc) (spec *Specification, err error) { spec = newSpec() - for _, d := range docs { - if len(d.parents) > 0 { - continue - } + buildDocumentGroups(docs, spec) - dg := NewDocGroup(d.Path.Relative) - setSpec(d, spec, dg) - } + indexCrossReferences(docs) - for _, d := range docs { - crossReferences := d.CrossReferences() - for id, xrefs := range crossReferences { - d.group.crossReferences[id] = append(d.group.crossReferences[id], xrefs...) - } - } - - for _, d := range docs { - var anchors map[string][]*Anchor - anchors, err = d.Anchors() - if err != nil { - return - } - for id, anchor := range anchors { - d.group.anchors[id] = append(d.group.anchors[id], anchor...) - } - for id, anchor := range d.anchorsByLabel { - d.group.anchorsByLabel[id] = append(d.group.anchorsByLabel[id], anchor...) - } + err = indexAnchors(docs) + if err != nil { + return } var basicInformationCluster, bridgedBasicInformationCluster *matter.Cluster @@ -91,8 +68,6 @@ func (sp *Builder) buildSpec(docs []*Doc) (spec *Specification, err error) { if err != nil { return } - case matter.DocTypeDataModel: - } } @@ -151,6 +126,24 @@ func (sp *Builder) buildSpec(docs []*Doc) (spec *Specification, err error) { return } + buildClusterReferences(spec) + associateDeviceTypeRequirementWithClusters(spec) + + return +} + +func buildDocumentGroups(docs []*Doc, spec *Specification) { + for _, d := range docs { + if len(d.parents) > 0 { + continue + } + + dg := NewDocGroup(d.Path.Relative) + setSpec(d, spec, dg) + } +} + +func buildClusterReferences(spec *Specification) { for _, c := range spec.ClustersByName { if c.Features != nil { spec.ClusterRefs.Add(c, c.Features) @@ -165,7 +158,35 @@ func (sp *Builder) buildSpec(docs []*Doc) (spec *Specification, err error) { spec.ClusterRefs.Add(c, en) } } +} +func indexAnchors(docs []*Doc) (err error) { + for _, d := range docs { + var anchors map[string][]*Anchor + anchors, err = d.Anchors() + if err != nil { + return + } + for id, anchor := range anchors { + d.group.anchors[id] = append(d.group.anchors[id], anchor...) + } + for id, anchor := range d.anchorsByLabel { + d.group.anchorsByLabel[id] = append(d.group.anchorsByLabel[id], anchor...) + } + } + return +} + +func indexCrossReferences(docs []*Doc) { + for _, d := range docs { + crossReferences := d.CrossReferences() + for id, xrefs := range crossReferences { + d.group.crossReferences[id] = append(d.group.crossReferences[id], xrefs...) + } + } +} + +func associateDeviceTypeRequirementWithClusters(spec *Specification) { for _, dt := range spec.DeviceTypes { for _, cr := range dt.ClusterRequirements { if c, ok := spec.ClustersByID[cr.ClusterID.Value()]; ok { @@ -183,7 +204,6 @@ func (sp *Builder) buildSpec(docs []*Doc) (spec *Specification, err error) { } } } - return } func addClusterToSpec(spec *Specification, d *Doc, m *matter.Cluster) { @@ -243,118 +263,6 @@ func addClusterToSpec(spec *Specification, d *Doc, m *matter.Cluster) { } } -func (sp *Builder) resolveDataTypeReferences(spec *Specification) { - for _, s := range spec.structIndex { - for _, f := range s.Fields { - sp.resolveDataType(spec, nil, f, f.Type) - } - } - for _, cluster := range spec.ClustersByName { - for _, a := range cluster.Attributes { - sp.resolveDataType(spec, cluster, a, a.Type) - } - for _, s := range cluster.Structs { - for _, f := range s.Fields { - sp.resolveDataType(spec, cluster, f, f.Type) - } - } - for _, s := range cluster.Events { - for _, f := range s.Fields { - sp.resolveDataType(spec, cluster, f, f.Type) - } - } - for _, s := range cluster.Commands { - for _, f := range s.Fields { - sp.resolveDataType(spec, cluster, f, f.Type) - } - } - } - -} - -func (sp *Builder) resolveDataType(spec *Specification, cluster *matter.Cluster, field *matter.Field, dataType *types.DataType) { - if dataType == nil { - if !conformance.IsDeprecated(field.Conformance) && (cluster == nil || cluster.Hierarchy == "Base") { - var clusterName string - if cluster != nil { - clusterName = cluster.Name - } - if !sp.IgnoreHierarchy { - slog.Warn("missing type on field", log.Path("path", field), slog.String("id", field.ID.HexString()), slog.String("name", field.Name), slog.String("cluster", clusterName)) - } - } - return - } - switch dataType.BaseType { - case types.BaseDataTypeTag: - getTagNamespace(spec, field) - case types.BaseDataTypeList: - sp.resolveDataType(spec, cluster, field, dataType.EntryType) - case types.BaseDataTypeCustom: - if dataType.Entity == nil { - dataType.Entity = getCustomDataType(spec, dataType.Name, cluster, field) - if dataType.Entity == nil { - slog.Error("unknown custom data type", slog.String("cluster", clusterName(cluster)), slog.String("field", field.Name), slog.String("type", dataType.Name), log.Path("source", field)) - } - } - if cluster == nil || dataType.Entity == nil { - return - } - spec.ClusterRefs.Add(cluster, dataType.Entity) - s, ok := dataType.Entity.(*matter.Struct) - if !ok { - return - } - for _, f := range s.Fields { - sp.resolveDataType(spec, cluster, f, f.Type) - } - } -} - -func getCustomDataType(spec *Specification, dataTypeName string, cluster *matter.Cluster, field *matter.Field) (e types.Entity) { - entities := spec.entities[dataTypeName] - if len(entities) == 0 { - canonicalName := CanonicalName(dataTypeName) - if canonicalName != dataTypeName { - e = getCustomDataType(spec, canonicalName, cluster, field) - } else { - e = getCustomDataTypeFromReference(spec, cluster, field) - } - } else if len(entities) == 1 { - for m := range entities { - e = m - } - } else { - e = disambiguateDataType(entities, cluster, field) - } - return -} - -func getCustomDataTypeFromReference(spec *Specification, cluster *matter.Cluster, field *matter.Field) (e types.Entity) { - switch source := field.Type.Source.(type) { - case *asciidoc.CrossReference: - doc, ok := spec.DocRefs[cluster] - if !ok { - return - } - anchor := doc.FindAnchor(source.ID) - if anchor == nil { - return - } - switch el := anchor.Element.(type) { - case *asciidoc.Section: - entities := doc.entitiesBySection[el] - if len(entities) == 1 { - e = entities[0] - return - } - } - return nil - default: - return - } -} - func getTagNamespace(spec *Specification, field *matter.Field) { for _, ns := range spec.Namespaces { if strings.EqualFold(ns.Name, field.Type.Name) { @@ -365,47 +273,6 @@ func getTagNamespace(spec *Specification, field *matter.Field) { slog.Warn("failed to match tag name space", slog.String("name", field.Name), log.Path("field", field), slog.String("namespace", field.Type.Name)) } -func disambiguateDataType(entities map[types.Entity]*matter.Cluster, cluster *matter.Cluster, field *matter.Field) types.Entity { - // If there are multiple entities with the same name, prefer the one on the current cluster - for m, c := range entities { - if c == cluster { - return m - } - } - - // OK, if the data type is defined on the direct parent of this cluster, take that one - if cluster.Parent != nil { - for m, c := range entities { - if c != nil && c == cluster.Parent { - return m - } - } - } - - var nakedEntities []types.Entity - for m, c := range entities { - if c == nil { - nakedEntities = append(nakedEntities, m) - } - } - if len(nakedEntities) == 1 { - return nakedEntities[0] - } - - // Can't disambiguate out this data model - slog.Warn("ambiguous data type", "cluster", clusterName(cluster), "field", field.Name, log.Path("source", field)) - for m, c := range entities { - var clusterName string - if c != nil { - clusterName = c.Name - } else { - clusterName = "naked" - } - slog.Warn("ambiguous data type", "model", m.Source(), "cluster", clusterName) - } - return nil -} - func clusterName(cluster *matter.Cluster) string { if cluster != nil { return cluster.Name diff --git a/matter/spec/cluster.go b/matter/spec/cluster.go index f5cb9d89..2ecc8ca3 100644 --- a/matter/spec/cluster.go +++ b/matter/spec/cluster.go @@ -84,6 +84,7 @@ func (s *Section) toClusters(d *Doc, entityMap map[asciidoc.Attributable][]types cg.AddBitmaps(bitmaps...) cg.AddEnums(enums...) cg.AddStructs(structs...) + cg.AddTypeDefs(typedefs...) } else { entities = append(entities, clusters[0]) } @@ -93,7 +94,7 @@ func (s *Section) toClusters(d *Doc, entityMap map[asciidoc.Attributable][]types c.AddBitmaps(bitmaps...) c.AddEnums(enums...) c.AddStructs(structs...) - c.TypeDefs = append(c.TypeDefs, typedefs...) + c.AddTypeDefs(typedefs...) c.Features = features for _, s := range elements { diff --git a/matter/spec/commands.go b/matter/spec/commands.go index e163f9fb..e396b290 100644 --- a/matter/spec/commands.go +++ b/matter/spec/commands.go @@ -34,7 +34,7 @@ func (cf *commandFactory) New(d *Doc, s *Section, ti *TableInfo, row *asciidoc.T return nil, err } cmd.Direction = ParseCommandDirection(dir) - cmd.Response, _ = d.ReadRowDataType(row, ti.ColumnMap, matter.TableColumnResponse) + cmd.Response, _ = ti.ReadDataType(row, matter.TableColumnResponse) if cmd.Response != nil { cmd.Response.Name = text.TrimCaseInsensitiveSuffix(cmd.Response.Name, " Command") } diff --git a/matter/spec/datatypes.go b/matter/spec/datatypes.go index c93aefce..07ae6f86 100644 --- a/matter/spec/datatypes.go +++ b/matter/spec/datatypes.go @@ -1,18 +1,14 @@ package spec import ( - "fmt" "log/slog" - "regexp" - "strings" "github.com/project-chip/alchemy/asciidoc" "github.com/project-chip/alchemy/errata" "github.com/project-chip/alchemy/internal/log" "github.com/project-chip/alchemy/internal/parse" - "github.com/project-chip/alchemy/internal/text" "github.com/project-chip/alchemy/matter" - "github.com/project-chip/alchemy/matter/constraint" + "github.com/project-chip/alchemy/matter/conformance" "github.com/project-chip/alchemy/matter/types" ) @@ -65,165 +61,155 @@ func (s *Section) toDataTypes(d *Doc, entityMap map[asciidoc.Attributable][]type return } -func (d *Doc) readFields(ti *TableInfo, entityType types.EntityType) (fields []*matter.Field, err error) { - ids := make(map[uint64]*matter.Field) - for row := range ti.Body() { - f := matter.NewField(row) - f.Name, err = ti.ReadValue(row, matter.TableColumnName) - if err != nil { - return +func (sp *Builder) resolveDataTypeReferences(spec *Specification) { + for _, s := range spec.structIndex { + for _, f := range s.Fields { + sp.resolveDataType(spec, nil, f, f.Type) } - f.Name = matter.StripTypeSuffixes(f.Name) - f.Conformance = ti.ReadConformance(row, matter.TableColumnConformance) - f.Type, err = d.ReadRowDataType(row, ti.ColumnMap, matter.TableColumnType) - if err != nil { - slog.Debug("error reading field data type", slog.String("path", d.Path.String()), slog.String("name", f.Name), slog.Any("error", err)) - err = nil + } + for cluster := range spec.Clusters { + for _, a := range cluster.Attributes { + sp.resolveDataType(spec, cluster, a, a.Type) } - - f.Constraint = ti.ReadConstraint(row, matter.TableColumnConstraint) - if err != nil { - return + for _, s := range cluster.Structs { + for _, f := range s.Fields { + sp.resolveDataType(spec, cluster, f, f.Type) + } } - var q string - q, err = ti.ReadString(row, matter.TableColumnQuality) - if err != nil { - return + for _, s := range cluster.Events { + for _, f := range s.Fields { + sp.resolveDataType(spec, cluster, f, f.Type) + } } - f.Quality = parseQuality(q, entityType, d, row) - f.Default, err = ti.ReadString(row, matter.TableColumnDefault) - if err != nil { - return + for _, s := range cluster.Commands { + for _, f := range s.Fields { + sp.resolveDataType(spec, cluster, f, f.Type) + } } + } - var a string - a, err = ti.ReadString(row, matter.TableColumnAccess) - if err != nil { +} + +func (sp *Builder) resolveDataType(spec *Specification, cluster *matter.Cluster, field *matter.Field, dataType *types.DataType) { + if dataType == nil { + if !conformance.IsDeprecated(field.Conformance) && (cluster == nil || cluster.Hierarchy == "Base") { + var clusterName string + if cluster != nil { + clusterName = cluster.Name + } + if !sp.IgnoreHierarchy { + slog.Warn("missing type on field", log.Path("path", field), slog.String("id", field.ID.HexString()), slog.String("name", field.Name), slog.String("cluster", clusterName)) + } + } + return + } + switch dataType.BaseType { + case types.BaseDataTypeTag: + getTagNamespace(spec, field) + case types.BaseDataTypeList: + sp.resolveDataType(spec, cluster, field, dataType.EntryType) + case types.BaseDataTypeCustom: + if dataType.Entity == nil { + dataType.Entity = getCustomDataType(spec, dataType.Name, cluster, field) + if dataType.Entity == nil { + slog.Error("unknown custom data type", slog.String("cluster", clusterName(cluster)), slog.String("field", field.Name), slog.String("type", dataType.Name), log.Path("source", field)) + } + } + if cluster == nil || dataType.Entity == nil { return } - f.Access, _ = ParseAccess(a, entityType) - f.ID, err = ti.ReadID(row, matter.TableColumnID) - if err != nil { + spec.ClusterRefs.Add(cluster, dataType.Entity) + s, ok := dataType.Entity.(*matter.Struct) + if !ok { return } - if f.ID.Valid() { - id := f.ID.Value() - existing, ok := ids[id] - if ok { - slog.Error("duplicate field ID", log.Path("source", f), slog.String("name", f.Name), slog.Uint64("id", id), log.Path("original", existing)) - continue - } - ids[id] = f + for _, f := range s.Fields { + sp.resolveDataType(spec, cluster, f, f.Type) } + } +} - if f.Type != nil { - var cs constraint.Set - switch f.Type.BaseType { - case types.BaseDataTypeMessageID: - cs = []constraint.Constraint{&constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 16}}} - case types.BaseDataTypeIPAddress: - cs = []constraint.Constraint{&constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 4}}, &constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 16}}} - case types.BaseDataTypeIPv4Address: - cs = []constraint.Constraint{&constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 4}}} - case types.BaseDataTypeIPv6Address: - cs = []constraint.Constraint{&constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 16}}} - case types.BaseDataTypeIPv6Prefix: - cs = []constraint.Constraint{&constraint.RangeConstraint{Minimum: &constraint.IntLimit{Value: 1}, Maximum: &constraint.IntLimit{Value: 17}}} - case types.BaseDataTypeHardwareAddress: - cs = []constraint.Constraint{&constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 6}}, &constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 8}}} - } - if cs != nil { - if f.Type.IsArray() { - lc, ok := f.Constraint.(*constraint.ListConstraint) - if ok { - lc.EntryConstraint = constraint.AppendConstraint(lc.EntryConstraint, cs...) - } - } else { - f.Constraint = constraint.AppendConstraint(f.Constraint, cs...) - } - - } +func getCustomDataType(spec *Specification, dataTypeName string, cluster *matter.Cluster, field *matter.Field) (e types.Entity) { + entities := spec.entities[dataTypeName] + if len(entities) == 0 { + canonicalName := CanonicalName(dataTypeName) + if canonicalName != dataTypeName { + e = getCustomDataType(spec, canonicalName, cluster, field) + } else { + e = getCustomDataTypeFromReference(spec, cluster, field) + } + } else if len(entities) == 1 { + for m := range entities { + e = m } - f.Name = CanonicalName(f.Name) - fields = append(fields, f) + } else { + e = disambiguateDataType(entities, cluster, field) } return } -var listDataTypeDefinitionPattern = regexp.MustCompile(`(?:list|List|DataTypeList)\[([^]]+)]`) -var asteriskPattern = regexp.MustCompile(`\^[0-9]+\^\s*$`) - -func (d *Doc) ReadRowDataType(row *asciidoc.TableRow, columnMap ColumnIndex, column matter.TableColumn) (*types.DataType, error) { - if !d.anchorsParsed { - d.findAnchors() - } - i, ok := columnMap[column] - if !ok { - return nil, fmt.Errorf("missing %s column for data type", column) - } - cell := row.Cell(i) - cellElements := cell.Elements() - if len(cellElements) == 0 { - return nil, fmt.Errorf("empty %s cell for data type", column) +func getCustomDataTypeFromReference(spec *Specification, cluster *matter.Cluster, field *matter.Field) (e types.Entity) { + switch source := field.Type.Source.(type) { + case *asciidoc.CrossReference: + doc, ok := spec.DocRefs[cluster] + if !ok { + return + } + anchor := doc.FindAnchor(source.ID) + if anchor == nil { + return + } + switch el := anchor.Element.(type) { + case *asciidoc.Section: + entities := doc.entitiesBySection[el] + if len(entities) == 1 { + e = entities[0] + return + } + } + return nil + default: + return } +} - var isArray bool +func disambiguateDataType(entities map[types.Entity]*matter.Cluster, cluster *matter.Cluster, field *matter.Field) types.Entity { + // If there are multiple entities with the same name, prefer the one on the current cluster + for m, c := range entities { + if c == cluster { + return m + } + } - var sb strings.Builder - source := d.buildDataTypeString(cellElements, &sb) - var name string - var content = asteriskPattern.ReplaceAllString(sb.String(), "") - match := listDataTypeDefinitionPattern.FindStringSubmatch(content) - if match != nil { - name = match[1] - isArray = true - } else { - name = content + // OK, if the data type is defined on the direct parent of this cluster, take that one + if cluster.Parent != nil { + for m, c := range entities { + if c != nil && c == cluster.Parent { + return m + } + } } - commaIndex := strings.IndexRune(name, ',') - if commaIndex >= 0 { - name = name[:commaIndex] + + var nakedEntities []types.Entity + for m, c := range entities { + if c == nil { + nakedEntities = append(nakedEntities, m) + } } - name = text.TrimCaseInsensitiveSuffix(name, " Type") - dt := types.ParseDataType(name, isArray) - if dt != nil { - dt.Source = source + if len(nakedEntities) == 1 { + return nakedEntities[0] } - return dt, nil -} - -func (d *Doc) buildDataTypeString(cellElements asciidoc.Set, sb *strings.Builder) (source asciidoc.Element) { - for _, el := range cellElements { - switch v := el.(type) { - case *asciidoc.String: - sb.WriteString(v.Value) - case *asciidoc.CrossReference: - if len(v.Set) > 0 { - d.buildDataTypeString(v.Set, sb) - } else { - var name string - anchor := d.FindAnchor(v.ID) - if anchor != nil { - name = ReferenceName(anchor.Element) - if len(name) == 0 { - name = asciidoc.AttributeAsciiDocString(anchor.LabelElements) - } - } else { - slog.Warn("data type references unknown or ambiguous anchor", slog.String("name", v.ID), log.Path("source", NewSource(d, v))) - } - if len(name) == 0 { - name = strings.TrimPrefix(v.ID, "_") - } - sb.WriteString(name) - } - source = el - case *asciidoc.SpecialCharacter: - case *asciidoc.Paragraph: - source = d.buildDataTypeString(v.Elements(), sb) - default: - slog.Warn("unknown data type value element", log.Element("path", d.Path, el), "type", fmt.Sprintf("%T", v)) + // Can't disambiguate out this data model + slog.Warn("ambiguous data type", "cluster", clusterName(cluster), "field", field.Name, log.Path("source", field)) + for m, c := range entities { + var clusterName string + if c != nil { + clusterName = c.Name + } else { + clusterName = "naked" } + slog.Warn("ambiguous data type", "model", m.Source(), "cluster", clusterName) } - return + return nil } diff --git a/matter/spec/field.go b/matter/spec/field.go index 1147808d..7a4d3407 100644 --- a/matter/spec/field.go +++ b/matter/spec/field.go @@ -11,9 +11,95 @@ import ( "github.com/project-chip/alchemy/internal/text" "github.com/project-chip/alchemy/matter" "github.com/project-chip/alchemy/matter/conformance" + "github.com/project-chip/alchemy/matter/constraint" "github.com/project-chip/alchemy/matter/types" ) +func (d *Doc) readFields(ti *TableInfo, entityType types.EntityType) (fields []*matter.Field, err error) { + ids := make(map[uint64]*matter.Field) + for row := range ti.Body() { + f := matter.NewField(row) + f.Name, err = ti.ReadValue(row, matter.TableColumnName) + if err != nil { + return + } + f.Name = matter.StripTypeSuffixes(f.Name) + f.Conformance = ti.ReadConformance(row, matter.TableColumnConformance) + f.Type, err = ti.ReadDataType(row, matter.TableColumnType) + if err != nil { + slog.Debug("error reading field data type", slog.String("path", d.Path.String()), slog.String("name", f.Name), slog.Any("error", err)) + err = nil + } + + f.Constraint = ti.ReadConstraint(row, matter.TableColumnConstraint) + if err != nil { + return + } + var q string + q, err = ti.ReadString(row, matter.TableColumnQuality) + if err != nil { + return + } + f.Quality = parseQuality(q, entityType, d, row) + f.Default, err = ti.ReadString(row, matter.TableColumnDefault) + if err != nil { + return + } + + var a string + a, err = ti.ReadString(row, matter.TableColumnAccess) + if err != nil { + return + } + f.Access, _ = ParseAccess(a, entityType) + f.ID, err = ti.ReadID(row, matter.TableColumnID) + if err != nil { + return + } + if f.ID.Valid() { + id := f.ID.Value() + existing, ok := ids[id] + if ok { + slog.Error("duplicate field ID", log.Path("source", f), slog.String("name", f.Name), slog.Uint64("id", id), log.Path("original", existing)) + continue + } + ids[id] = f + } + + if f.Type != nil { + var cs constraint.Set + switch f.Type.BaseType { + case types.BaseDataTypeMessageID: + cs = []constraint.Constraint{&constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 16}}} + case types.BaseDataTypeIPAddress: + cs = []constraint.Constraint{&constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 4}}, &constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 16}}} + case types.BaseDataTypeIPv4Address: + cs = []constraint.Constraint{&constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 4}}} + case types.BaseDataTypeIPv6Address: + cs = []constraint.Constraint{&constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 16}}} + case types.BaseDataTypeIPv6Prefix: + cs = []constraint.Constraint{&constraint.RangeConstraint{Minimum: &constraint.IntLimit{Value: 1}, Maximum: &constraint.IntLimit{Value: 17}}} + case types.BaseDataTypeHardwareAddress: + cs = []constraint.Constraint{&constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 6}}, &constraint.ExactConstraint{Value: &constraint.IntLimit{Value: 8}}} + } + if cs != nil { + if f.Type.IsArray() { + lc, ok := f.Constraint.(*constraint.ListConstraint) + if ok { + lc.EntryConstraint = constraint.AppendConstraint(lc.EntryConstraint, cs...) + } + } else { + f.Constraint = constraint.AppendConstraint(f.Constraint, cs...) + } + + } + } + f.Name = CanonicalName(f.Name) + fields = append(fields, f) + } + return +} + func (s *Section) mapFields(fieldMap map[string]*matter.Field, entityMap map[asciidoc.Attributable][]types.Entity) error { for _, s := range parse.Skim[*Section](s.Elements()) { var name string diff --git a/matter/spec/table.go b/matter/spec/table.go index 52662ed0..7f1d8464 100644 --- a/matter/spec/table.go +++ b/matter/spec/table.go @@ -3,59 +3,14 @@ package spec import ( "context" "fmt" - "iter" - "log/slog" - "strconv" "strings" "github.com/project-chip/alchemy/asciidoc" "github.com/project-chip/alchemy/asciidoc/render" - "github.com/project-chip/alchemy/internal/log" "github.com/project-chip/alchemy/internal/parse" "github.com/project-chip/alchemy/matter" - "github.com/project-chip/alchemy/matter/conformance" - "github.com/project-chip/alchemy/matter/constraint" ) -type ColumnIndex map[matter.TableColumn]int - -func (ci ColumnIndex) HasAny(columns ...matter.TableColumn) bool { - for _, col := range columns { - _, ok := ci[col] - if ok { - return true - } - } - return false -} - -func (ci ColumnIndex) HasAll(columns ...matter.TableColumn) bool { - if len(columns) == 0 { - return false - } - for _, col := range columns { - _, ok := ci[col] - if !ok { - return false - } - } - return true -} - -type ExtraColumn struct { - Name string - Offset int -} - -type TableInfo struct { - Doc *Doc - Element *asciidoc.Table - Rows []*asciidoc.TableRow - HeaderRowIndex int - ColumnMap ColumnIndex - ExtraColumns []ExtraColumn -} - var ErrNoTableFound = fmt.Errorf("no table found") func parseFirstTable(doc *Doc, section *Section) (ti *TableInfo, err error) { @@ -84,310 +39,6 @@ func parseTable(doc *Doc, section *Section, t *asciidoc.Table) (ti *TableInfo, e return } -func (ti *TableInfo) ReadString(row *asciidoc.TableRow, columns ...matter.TableColumn) (string, error) { - for _, column := range columns { - offset, ok := ti.ColumnMap[column] - if !ok { - continue - } - cell := row.Cell(offset) - val, err := RenderTableCell(cell) - if err != nil { - return "", err - } - val = asteriskPattern.ReplaceAllString(val, "") - return val, nil - } - return "", nil -} - -func (ti *TableInfo) ReadStringAtOffset(row *asciidoc.TableRow, offset int) (string, error) { - cell := row.Cell(offset) - val, err := RenderTableCell(cell) - if err != nil { - return "", err - } - val = asteriskPattern.ReplaceAllString(val, "") - return val, nil -} - -func (ti *TableInfo) ReadID(row *asciidoc.TableRow, columns ...matter.TableColumn) (*matter.Number, error) { - id, err := ti.ReadString(row, columns...) - if err != nil { - return matter.InvalidID, err - } - return matter.ParseNumber(id), nil -} - -func (ti *TableInfo) ReadName(row *asciidoc.TableRow, columns ...matter.TableColumn) (name string, xref *asciidoc.CrossReference, err error) { - for _, column := range columns { - offset, ok := ti.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(ti.Doc, cellElements, &value) - if err != nil { - return "", nil, err - } - return strings.TrimSpace(value.String()), xref, nil - } - return "", nil, nil -} - -func (ti *TableInfo) ReadValue(row *asciidoc.TableRow, columns ...matter.TableColumn) (string, error) { - for _, column := range columns { - offset, ok := ti.ColumnMap[column] - if !ok { - continue - } - return ti.ReadValueByIndex(row, offset) - } - return "", nil -} - -func (ti *TableInfo) ReadValueByIndex(row *asciidoc.TableRow, offset int) (string, error) { - cell := row.Cell(offset) - cellElements := cell.Elements() - if len(cellElements) == 0 { - return "", nil - } - var value strings.Builder - err := readRowCellValueElements(ti.Doc, cellElements, &value) - if err != nil { - return "", err - } - return strings.TrimSpace(value.String()), nil -} - -type CellRenderer func(cellElements asciidoc.Set, sb *strings.Builder) (source asciidoc.Element) - -func (ti *TableInfo) RenderColumn(row *asciidoc.TableRow, renderer CellRenderer, columns ...matter.TableColumn) (value string, source asciidoc.Element, ok bool) { - for _, column := range columns { - var offset int - offset, ok = ti.ColumnMap[column] - if !ok { - continue - } - cell := row.Cell(offset) - cellElements := cell.Elements() - if len(cellElements) == 0 { - ok = false - return - } - source = cell - var sb strings.Builder - sourceOverride := renderer(cellElements, &sb) - if sourceOverride != nil { - source = sourceOverride - } - value = sb.String() - return - } - return -} - -var newLineReplacer = strings.NewReplacer("\r\n", "", "\r", "", "\n", "") - -func (ti *TableInfo) ReadConformance(row *asciidoc.TableRow, column matter.TableColumn) conformance.Set { - val, _, ok := ti.RenderColumn(row, ti.buildRowConformance, column) - if !ok { - return nil - } - - s := strings.TrimSpace(val) - if len(s) == 0 { - return conformance.Set{&conformance.Mandatory{}} - } - s = newLineReplacer.Replace(s) - return conformance.ParseConformance(matter.StripTypeSuffixes(s)) -} - -func (ti *TableInfo) buildRowConformance(cellElements asciidoc.Set, sb *strings.Builder) (source asciidoc.Element) { - for _, el := range cellElements { - switch v := el.(type) { - case *asciidoc.String: - sb.WriteString(v.Value) - case *asciidoc.CrossReference: - id := v.ID - if strings.HasPrefix(id, "ref_") { - // This is a proper reference; allow the conformance parser to recognize it - sb.WriteString(fmt.Sprintf("<<%s>>", id)) - } else { - anchor := ti.Doc.FindAnchor(v.ID) - var name string - if anchor != nil { - name = ReferenceName(anchor.Element) - } else { - name = strings.TrimPrefix(v.ID, "_") - } - sb.WriteString(name) - } - case *asciidoc.SpecialCharacter: - sb.WriteString(v.Character) - case *asciidoc.Superscript: - // This is usually an asterisk, and should be ignored - case *asciidoc.Link: - sb.WriteString(v.URL.Scheme) - ti.buildRowConformance(v.URL.Path, sb) - case *asciidoc.LinkMacro: - sb.WriteString("link:") - sb.WriteString(v.URL.Scheme) - ti.buildRowConformance(v.URL.Path, sb) - case *asciidoc.CharacterReplacementReference: - switch v.Name() { - case "nbsp": - sb.WriteRune(' ') - default: - slog.Warn("unknown predefined attribute", log.Element("path", ti.Doc.Path, el), "name", v.Name) - } - case *asciidoc.NewLine: - sb.WriteRune(' ') - case asciidoc.HasElements: - ti.buildRowConformance(v.Elements(), sb) - default: - slog.Warn("unknown conformance value element", log.Element("path", ti.Doc.Path, el), "type", fmt.Sprintf("%T", el)) - } - } - return -} - -func (ti *TableInfo) ReadConstraint(row *asciidoc.TableRow, column matter.TableColumn) constraint.Constraint { - - val, source, _ := ti.RenderColumn(row, ti.buildConstraintValue, column) - s := strings.TrimSpace(val) - s = strings.ReplaceAll(s, "\n", " ") - var c constraint.Constraint - c, err := constraint.ParseString(s) - if err != nil { - slog.Error("failed parsing constraint cell", log.Element("path", ti.Doc.Path, source), slog.String("constraint", val)) - return &constraint.GenericConstraint{Value: val} - } - return c -} - -func (ti *TableInfo) buildConstraintValue(els asciidoc.Set, sb *strings.Builder) (source asciidoc.Element) { - for _, el := range els { - switch v := el.(type) { - case *asciidoc.String: - sb.WriteString(v.Value) - case *asciidoc.CrossReference: - anchor := ti.Doc.FindAnchor(v.ID) - var name string - if anchor != nil { - name = matter.StripReferenceSuffixes(ReferenceName(anchor.Element)) - } else { - name = strings.TrimPrefix(v.ID, "_") - } - sb.WriteString(name) - case *asciidoc.Superscript: - var qt strings.Builder - ti.buildConstraintValue(v.Elements(), &qt) - val := qt.String() - if val == "*" { // We ignore asterisks here - continue - } - sb.WriteString("^") - sb.WriteString(val) - sb.WriteString("^") - case *asciidoc.Bold: // This is usually an asterisk, and should be ignored - case *asciidoc.NewLine, *asciidoc.LineBreak: - sb.WriteRune(' ') - case asciidoc.HasElements: - ti.buildConstraintValue(v.Elements(), sb) - case asciidoc.AttributeReference: - sb.WriteString(fmt.Sprintf("{%s}", v.Name())) - default: - slog.Warn("unknown constraint value element", log.Element("path", ti.Doc.Path, el), "type", fmt.Sprintf("%T", el)) - } - } - return -} - -func readRowCellValueElements(doc *Doc, els asciidoc.Set, value *strings.Builder) (err error) { - for _, el := range els { - switch el := el.(type) { - case *asciidoc.String: - value.WriteString(el.Value) - case asciidoc.FormattedTextElement: - err = readRowCellValueElements(doc, el.Elements(), value) - case *asciidoc.Paragraph: - err = readRowCellValueElements(doc, el.Elements(), value) - case *asciidoc.CrossReference: - if len(el.Set) > 0 { - readRowCellValueElements(doc, el.Set, value) - } else { - var val string - anchor := doc.FindAnchor(el.ID) - if anchor != nil { - val = matter.StripTypeSuffixes(ReferenceName(anchor.Element)) - } else { - val = strings.TrimPrefix(el.ID, "_") - val = strings.TrimPrefix(val, "ref_") // Trim, and hope someone else has it defined - } - value.WriteString(val) - } - case *asciidoc.Link: - value.WriteString(el.URL.Scheme) - readRowCellValueElements(doc, el.URL.Path, value) - case *asciidoc.LinkMacro: - value.WriteString(el.URL.Scheme) - readRowCellValueElements(doc, el.URL.Path, value) - case *asciidoc.Superscript: - // In the special case of superscript elements, we do checks to make sure it's not an asterisk or a footnote, which should be ignored - var quotedText strings.Builder - err = readRowCellValueElements(doc, el.Elements(), "edText) - if err != nil { - return - } - qt := quotedText.String() - if qt == "*" { // - continue - } - _, parseErr := strconv.Atoi(qt) - if parseErr == nil { - // This is probably a footnote - // The similar buildConstraintValue method does not do this, as there are exponential values in contraints - continue - } - value.WriteString(qt) - case *asciidoc.SpecialCharacter: - value.WriteString(el.Character) - case *asciidoc.InlinePassthrough: - value.WriteString("+") - err = readRowCellValueElements(doc, el.Elements(), value) - case *asciidoc.InlineDoublePassthrough: - value.WriteString("++") - err = readRowCellValueElements(doc, el.Elements(), value) - case *asciidoc.ThematicBreak: - case asciidoc.EmptyLine: - case *asciidoc.NewLine: - value.WriteString(" ") - case asciidoc.HasElements: - err = readRowCellValueElements(doc, el.Elements(), value) - case *asciidoc.LineBreak: - value.WriteString(" ") - default: - return fmt.Errorf("unexpected type in cell: %T", el) - } - if err != nil { - return err - } - } - return nil -} - func FindFirstTable(section *Section) *asciidoc.Table { var table *asciidoc.Table parse.SkimFunc(section.Elements(), func(t *asciidoc.Table) bool { @@ -423,32 +74,6 @@ func (d *Doc) GetHeaderCellString(cell *asciidoc.TableCell) (string, error) { return v.String(), nil } -func (ti *TableInfo) ColumnIndex(columns ...matter.TableColumn) (index int, ok bool) { - for _, column := range columns { - index, ok = ti.ColumnMap[column] - if ok { - return - } - } - return -} - -func (ti *TableInfo) Rescan(doc *Doc) (err error) { - ti.HeaderRowIndex, ti.ColumnMap, ti.ExtraColumns, err = mapTableColumns(doc, ti.Rows) - return -} - -func (ti *TableInfo) Body() iter.Seq[*asciidoc.TableRow] { - return func(yield func(*asciidoc.TableRow) bool) { - for i := ti.HeaderRowIndex + 1; i < len(ti.Rows); i++ { - if !yield(ti.Rows[i]) { - return - } - } - - } -} - func ReadTable(doc *Doc, table *asciidoc.Table) (ti *TableInfo, err error) { ti = &TableInfo{Doc: doc, Element: table, Rows: table.TableRows()} ti.HeaderRowIndex, ti.ColumnMap, ti.ExtraColumns, err = mapTableColumns(doc, ti.Rows) diff --git a/matter/spec/table_info.go b/matter/spec/table_info.go new file mode 100644 index 00000000..78c77480 --- /dev/null +++ b/matter/spec/table_info.go @@ -0,0 +1,540 @@ +package spec + +import ( + "fmt" + "iter" + "log/slog" + "regexp" + "strconv" + "strings" + + "github.com/project-chip/alchemy/asciidoc" + "github.com/project-chip/alchemy/internal/log" + "github.com/project-chip/alchemy/internal/text" + "github.com/project-chip/alchemy/matter" + "github.com/project-chip/alchemy/matter/conformance" + "github.com/project-chip/alchemy/matter/constraint" + "github.com/project-chip/alchemy/matter/types" +) + +type ColumnIndex map[matter.TableColumn]int + +func (ci ColumnIndex) HasAny(columns ...matter.TableColumn) bool { + for _, col := range columns { + _, ok := ci[col] + if ok { + return true + } + } + return false +} + +func (ci ColumnIndex) HasAll(columns ...matter.TableColumn) bool { + if len(columns) == 0 { + return false + } + for _, col := range columns { + _, ok := ci[col] + if !ok { + return false + } + } + return true +} + +type ExtraColumn struct { + Name string + Offset int +} + +type TableInfo struct { + Doc *Doc + Element *asciidoc.Table + Rows []*asciidoc.TableRow + HeaderRowIndex int + ColumnMap ColumnIndex + ExtraColumns []ExtraColumn +} + +func (ti *TableInfo) ColumnIndex(columns ...matter.TableColumn) (index int, ok bool) { + for _, column := range columns { + index, ok = ti.ColumnMap[column] + if ok { + return + } + } + return +} + +func (ti *TableInfo) Rescan(doc *Doc) (err error) { + ti.HeaderRowIndex, ti.ColumnMap, ti.ExtraColumns, err = mapTableColumns(doc, ti.Rows) + return +} + +func (ti *TableInfo) Body() iter.Seq[*asciidoc.TableRow] { + return func(yield func(*asciidoc.TableRow) bool) { + for i := ti.HeaderRowIndex + 1; i < len(ti.Rows); i++ { + if !yield(ti.Rows[i]) { + return + } + } + + } +} + +func (ti *TableInfo) ReadString(row *asciidoc.TableRow, columns ...matter.TableColumn) (string, error) { + for _, column := range columns { + offset, ok := ti.ColumnMap[column] + if !ok { + continue + } + cell := row.Cell(offset) + val, err := RenderTableCell(cell) + if err != nil { + return "", err + } + val = asteriskPattern.ReplaceAllString(val, "") + return val, nil + } + return "", nil +} + +func (ti *TableInfo) ReadStringAtOffset(row *asciidoc.TableRow, offset int) (string, error) { + cell := row.Cell(offset) + val, err := RenderTableCell(cell) + if err != nil { + return "", err + } + val = asteriskPattern.ReplaceAllString(val, "") + return val, nil +} + +func (ti *TableInfo) ReadID(row *asciidoc.TableRow, columns ...matter.TableColumn) (*matter.Number, error) { + id, err := ti.ReadString(row, columns...) + if err != nil { + return matter.InvalidID, err + } + return matter.ParseNumber(id), nil +} + +func (ti *TableInfo) ReadName(row *asciidoc.TableRow, columns ...matter.TableColumn) (name string, xref *asciidoc.CrossReference, err error) { + for _, column := range columns { + offset, ok := ti.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(ti.Doc, cellElements, &value) + if err != nil { + return "", nil, err + } + return strings.TrimSpace(value.String()), xref, nil + } + return "", nil, nil +} + +func (ti *TableInfo) ReadValue(row *asciidoc.TableRow, columns ...matter.TableColumn) (string, error) { + for _, column := range columns { + offset, ok := ti.ColumnMap[column] + if !ok { + continue + } + return ti.ReadValueByIndex(row, offset) + } + return "", nil +} + +func (ti *TableInfo) ReadValueByIndex(row *asciidoc.TableRow, offset int) (string, error) { + cell := row.Cell(offset) + cellElements := cell.Elements() + if len(cellElements) == 0 { + return "", nil + } + var value strings.Builder + err := readRowCellValueElements(ti.Doc, cellElements, &value) + if err != nil { + return "", err + } + return strings.TrimSpace(value.String()), nil +} + +type CellRenderer func(cellElements asciidoc.Set, sb *strings.Builder) (source asciidoc.Element) + +func (ti *TableInfo) RenderColumn(row *asciidoc.TableRow, renderer CellRenderer, columns ...matter.TableColumn) (value string, source asciidoc.Element, ok bool) { + for _, column := range columns { + var offset int + offset, ok = ti.ColumnMap[column] + if !ok { + continue + } + cell := row.Cell(offset) + cellElements := cell.Elements() + if len(cellElements) == 0 { + ok = false + return + } + source = cell + var sb strings.Builder + sourceOverride := renderer(cellElements, &sb) + if sourceOverride != nil { + source = sourceOverride + } + value = sb.String() + return + } + return +} + +var newLineReplacer = strings.NewReplacer("\r\n", "", "\r", "", "\n", "") + +func (ti *TableInfo) ReadConformance(row *asciidoc.TableRow, column matter.TableColumn) conformance.Set { + val, _, ok := ti.RenderColumn(row, ti.buildRowConformance, column) + if !ok { + return nil + } + + s := strings.TrimSpace(val) + if len(s) == 0 { + return conformance.Set{&conformance.Mandatory{}} + } + s = newLineReplacer.Replace(s) + return conformance.ParseConformance(matter.StripTypeSuffixes(s)) +} + +func (ti *TableInfo) buildRowConformance(cellElements asciidoc.Set, sb *strings.Builder) (source asciidoc.Element) { + for _, el := range cellElements { + switch v := el.(type) { + case *asciidoc.String: + sb.WriteString(v.Value) + case *asciidoc.CrossReference: + id := v.ID + if strings.HasPrefix(id, "ref_") { + // This is a proper reference; allow the conformance parser to recognize it + sb.WriteString(fmt.Sprintf("<<%s>>", id)) + } else { + anchor := ti.Doc.FindAnchor(v.ID) + var name string + if anchor != nil { + name = ReferenceName(anchor.Element) + } else { + name = strings.TrimPrefix(v.ID, "_") + } + sb.WriteString(name) + } + case *asciidoc.SpecialCharacter: + sb.WriteString(v.Character) + case *asciidoc.Superscript: + // This is usually an asterisk, and should be ignored + case *asciidoc.Link: + sb.WriteString(v.URL.Scheme) + ti.buildRowConformance(v.URL.Path, sb) + case *asciidoc.LinkMacro: + sb.WriteString("link:") + sb.WriteString(v.URL.Scheme) + ti.buildRowConformance(v.URL.Path, sb) + case *asciidoc.CharacterReplacementReference: + switch v.Name() { + case "nbsp": + sb.WriteRune(' ') + default: + slog.Warn("unknown predefined attribute", log.Element("path", ti.Doc.Path, el), "name", v.Name) + } + case *asciidoc.NewLine: + sb.WriteRune(' ') + case asciidoc.HasElements: + ti.buildRowConformance(v.Elements(), sb) + default: + slog.Warn("unknown conformance value element", log.Element("path", ti.Doc.Path, el), "type", fmt.Sprintf("%T", el)) + } + } + return +} + +func (ti *TableInfo) ReadConstraint(row *asciidoc.TableRow, column matter.TableColumn) constraint.Constraint { + + val, source, _ := ti.RenderColumn(row, ti.buildConstraintValue, column) + s := strings.TrimSpace(val) + s = strings.ReplaceAll(s, "\n", " ") + var c constraint.Constraint + c, err := constraint.ParseString(s) + if err != nil { + slog.Error("failed parsing constraint cell", log.Element("path", ti.Doc.Path, source), slog.String("constraint", val)) + return &constraint.GenericConstraint{Value: val} + } + return c +} + +func (ti *TableInfo) buildConstraintValue(els asciidoc.Set, sb *strings.Builder) (source asciidoc.Element) { + for _, el := range els { + switch v := el.(type) { + case *asciidoc.String: + sb.WriteString(v.Value) + case *asciidoc.CrossReference: + anchor := ti.Doc.FindAnchor(v.ID) + var name string + if anchor != nil { + name = matter.StripReferenceSuffixes(ReferenceName(anchor.Element)) + } else { + name = strings.TrimPrefix(v.ID, "_") + } + sb.WriteString(name) + case *asciidoc.Superscript: + var qt strings.Builder + ti.buildConstraintValue(v.Elements(), &qt) + val := qt.String() + if val == "*" { // We ignore asterisks here + continue + } + sb.WriteString("^") + sb.WriteString(val) + sb.WriteString("^") + case *asciidoc.Bold: // This is usually an asterisk, and should be ignored + case *asciidoc.NewLine, *asciidoc.LineBreak: + sb.WriteRune(' ') + case asciidoc.HasElements: + ti.buildConstraintValue(v.Elements(), sb) + case asciidoc.AttributeReference: + sb.WriteString(fmt.Sprintf("{%s}", v.Name())) + default: + slog.Warn("unknown constraint value element", log.Element("path", ti.Doc.Path, el), "type", fmt.Sprintf("%T", el)) + } + } + return +} + +func readRowCellValueElements(doc *Doc, els asciidoc.Set, value *strings.Builder) (err error) { + for _, el := range els { + switch el := el.(type) { + case *asciidoc.String: + value.WriteString(el.Value) + case asciidoc.FormattedTextElement: + err = readRowCellValueElements(doc, el.Elements(), value) + case *asciidoc.Paragraph: + err = readRowCellValueElements(doc, el.Elements(), value) + case *asciidoc.CrossReference: + if len(el.Set) > 0 { + readRowCellValueElements(doc, el.Set, value) + } else { + var val string + anchor := doc.FindAnchor(el.ID) + if anchor != nil { + val = matter.StripTypeSuffixes(ReferenceName(anchor.Element)) + } else { + val = strings.TrimPrefix(el.ID, "_") + val = strings.TrimPrefix(val, "ref_") // Trim, and hope someone else has it defined + } + value.WriteString(val) + } + case *asciidoc.Link: + value.WriteString(el.URL.Scheme) + readRowCellValueElements(doc, el.URL.Path, value) + case *asciidoc.LinkMacro: + value.WriteString(el.URL.Scheme) + readRowCellValueElements(doc, el.URL.Path, value) + case *asciidoc.Superscript: + // In the special case of superscript elements, we do checks to make sure it's not an asterisk or a footnote, which should be ignored + var quotedText strings.Builder + err = readRowCellValueElements(doc, el.Elements(), "edText) + if err != nil { + return + } + qt := quotedText.String() + if qt == "*" { // + continue + } + _, parseErr := strconv.Atoi(qt) + if parseErr == nil { + // This is probably a footnote + // The similar buildConstraintValue method does not do this, as there are exponential values in contraints + continue + } + value.WriteString(qt) + case *asciidoc.SpecialCharacter: + value.WriteString(el.Character) + case *asciidoc.InlinePassthrough: + value.WriteString("+") + err = readRowCellValueElements(doc, el.Elements(), value) + case *asciidoc.InlineDoublePassthrough: + value.WriteString("++") + err = readRowCellValueElements(doc, el.Elements(), value) + case *asciidoc.ThematicBreak: + case asciidoc.EmptyLine: + case *asciidoc.NewLine: + value.WriteString(" ") + case asciidoc.HasElements: + err = readRowCellValueElements(doc, el.Elements(), value) + case *asciidoc.LineBreak: + value.WriteString(" ") + default: + return fmt.Errorf("unexpected type in cell: %T", el) + } + if err != nil { + return err + } + } + return nil +} + +var listDataTypeDefinitionPattern = regexp.MustCompile(`(?:list|List|DataTypeList)\[([^]]+)]`) +var asteriskPattern = regexp.MustCompile(`\^[0-9]+\^\s*$`) + +func (ti *TableInfo) ReadDataType(row *asciidoc.TableRow, column matter.TableColumn) (*types.DataType, error) { + if !ti.Doc.anchorsParsed { + ti.Doc.findAnchors() + } + i, ok := ti.ColumnMap[column] + if !ok { + return nil, fmt.Errorf("missing %s column for data type", column) + } + cell := row.Cell(i) + cellElements := cell.Elements() + if len(cellElements) == 0 { + return nil, fmt.Errorf("empty %s cell for data type", column) + } + + var isArray bool + + var sb strings.Builder + source := ti.buildDataTypeString(cellElements, &sb) + var name string + var content = asteriskPattern.ReplaceAllString(sb.String(), "") + match := listDataTypeDefinitionPattern.FindStringSubmatch(content) + if match != nil { + name = match[1] + isArray = true + } else { + name = content + } + commaIndex := strings.IndexRune(name, ',') + if commaIndex >= 0 { + name = name[:commaIndex] + } + name = text.TrimCaseInsensitiveSuffix(name, " Type") + dt := types.ParseDataType(name, isArray) + if dt != nil { + dt.Source = source + } + return dt, nil +} + +func (ti *TableInfo) buildDataTypeString(cellElements asciidoc.Set, sb *strings.Builder) (source asciidoc.Element) { + for _, el := range cellElements { + switch v := el.(type) { + case *asciidoc.String: + sb.WriteString(v.Value) + case *asciidoc.CrossReference: + if len(v.Set) > 0 { + ti.buildDataTypeString(v.Set, sb) + + } else { + var name string + anchor := ti.Doc.FindAnchor(v.ID) + if anchor != nil { + name = ReferenceName(anchor.Element) + if len(name) == 0 { + name = asciidoc.AttributeAsciiDocString(anchor.LabelElements) + } + } else { + slog.Warn("data type references unknown or ambiguous anchor", slog.String("name", v.ID), log.Path("source", NewSource(ti.Doc, v))) + } + if len(name) == 0 { + name = strings.TrimPrefix(v.ID, "_") + } + sb.WriteString(name) + } + source = el + case *asciidoc.SpecialCharacter: + case *asciidoc.Paragraph: + source = ti.buildDataTypeString(v.Elements(), sb) + default: + slog.Warn("unknown data type value element", log.Element("path", ti.Doc.Path, el), "type", fmt.Sprintf("%T", v)) + } + } + return +} + +func (d *Doc) ReadRowDataType(row *asciidoc.TableRow, columnMap ColumnIndex, column matter.TableColumn) (*types.DataType, error) { + if !d.anchorsParsed { + d.findAnchors() + } + i, ok := columnMap[column] + if !ok { + return nil, fmt.Errorf("missing %s column for data type", column) + } + cell := row.Cell(i) + cellElements := cell.Elements() + if len(cellElements) == 0 { + return nil, fmt.Errorf("empty %s cell for data type", column) + } + + var isArray bool + + var sb strings.Builder + source := d.buildDataTypeString(cellElements, &sb) + var name string + var content = asteriskPattern.ReplaceAllString(sb.String(), "") + match := listDataTypeDefinitionPattern.FindStringSubmatch(content) + if match != nil { + name = match[1] + isArray = true + } else { + name = content + } + commaIndex := strings.IndexRune(name, ',') + if commaIndex >= 0 { + name = name[:commaIndex] + } + name = text.TrimCaseInsensitiveSuffix(name, " Type") + dt := types.ParseDataType(name, isArray) + if dt != nil { + dt.Source = source + } + return dt, nil +} + +func (d *Doc) buildDataTypeString(cellElements asciidoc.Set, sb *strings.Builder) (source asciidoc.Element) { + for _, el := range cellElements { + switch v := el.(type) { + case *asciidoc.String: + sb.WriteString(v.Value) + case *asciidoc.CrossReference: + if len(v.Set) > 0 { + d.buildDataTypeString(v.Set, sb) + + } else { + var name string + anchor := d.FindAnchor(v.ID) + if anchor != nil { + name = ReferenceName(anchor.Element) + if len(name) == 0 { + name = asciidoc.AttributeAsciiDocString(anchor.LabelElements) + } + } else { + slog.Warn("data type references unknown or ambiguous anchor", slog.String("name", v.ID), log.Path("source", NewSource(d, v))) + } + if len(name) == 0 { + name = strings.TrimPrefix(v.ID, "_") + } + sb.WriteString(name) + } + source = el + case *asciidoc.SpecialCharacter: + case *asciidoc.Paragraph: + source = d.buildDataTypeString(v.Elements(), sb) + default: + slog.Warn("unknown data type value element", log.Element("path", d.Path, el), "type", fmt.Sprintf("%T", v)) + } + } + return +} diff --git a/matter/typedef.go b/matter/typedef.go index 961d7d75..693d55f5 100644 --- a/matter/typedef.go +++ b/matter/typedef.go @@ -7,6 +7,8 @@ import ( type TypeDef struct { entity + ParentEntity types.Entity `json:"-"` + Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` Type *types.DataType `json:"type,omitempty"`