sq/drivers/userdriver/def.go
Neil O'Toole f9c19785e3
Almost at the end of the linting road (#120)
* yet more linting

* yet more linting

* yet more linting

* yet more linting

* yet more linting

* yet more linting

* yet more linting

* yet more linting
2022-12-17 23:42:11 -07:00

363 lines
9.7 KiB
Go

package userdriver
import (
"fmt"
"strings"
"github.com/neilotoole/sq/libsq/core/errz"
"github.com/neilotoole/sq/libsq/core/kind"
"github.com/neilotoole/sq/libsq/core/sqlmodel"
"github.com/neilotoole/sq/libsq/core/stringz"
)
// DriverDef is a user-defined driver definition.
type DriverDef struct {
// Name is short name of the driver type, e.g. "rss".
Name string `yaml:"driver" json:"driver"`
// Genre is the generic document type, e.g. XML or JSON etc.
Genre string `yaml:"genre" json:"genre"`
// Title is the full name of the driver
// type, e.g. "RSS (Really Simple Syndication)".
Title string `yaml:"title" json:"title"`
// Doc typically has a link to documentation for the driver..
Doc string `yaml:"doc,omitempty" json:"doc,omitempty"`
// Selector is the root doc element, e.g. "/rss"
Selector string `yaml:"selector" json:"selector"`
// Tables is the set of tables that define the type.
Tables []*TableMapping `yaml:"tables" json:"tables"`
}
// TableBySelector returns the TableMapping that matches sel, or nil.
func (d *DriverDef) TableBySelector(sel string) *TableMapping {
for _, t := range d.Tables {
if t.Selector == sel {
return t
}
}
return nil
}
func (d *DriverDef) String() string {
return stringz.SprintJSON(d)
}
// TableMapping describes how document data is mapped to a table.
type TableMapping struct {
// Name is the table name.
Name string `yaml:"table" json:"table"`
// Selector specifies how the table data is selected.
Selector string `yaml:"selector" json:"selector"`
// Cols is the set of columns in the table.
Cols []*ColMapping `yaml:"cols" json:"cols"`
// PrimaryKey is a slice of the column names that constitute
// the primary key. Typically this is one column, but can be
// more than one for composite primary keys.
PrimaryKey []string `yaml:"primary_key" json:"primary_key"`
// Comment is an optional table comment.
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
func (t *TableMapping) String() string {
return stringz.SprintJSON(t)
}
// ColBySelector returns the ColMapping associated with the element, or nil if no such col.
func (t *TableMapping) ColBySelector(sel string) *ColMapping {
if sel == "" {
return nil
}
for _, col := range t.Cols {
if sel == t.absColSelector(col) {
return col
}
}
for _, col := range t.Cols {
if sel == t.Selector+"/"+col.Name {
return col
}
}
return nil
}
// absColSelector returns an absolute (fully-qualified) selector for the provided ColMapping. For
// example, if ColMapping.Selector is "./item", the return value might be "/rss/channel/item".
func (t *TableMapping) absColSelector(col *ColMapping) string {
if col.Selector == "" {
return ""
}
if col.Selector[0] == '/' {
return col.Selector
}
if strings.HasPrefix(col.Selector, "./") {
return t.Selector + col.Selector[1:]
}
return t.Selector + "/" + col.Selector
}
// PKCols returns the cols that constitute this table's primary key,
// or an error if none defined. If error is non-nil, the returned
// slice will contain at least one ColMapping.
func (t *TableMapping) PKCols() ([]*ColMapping, error) {
var cols []*ColMapping
for i := range t.Cols {
for j := range t.PrimaryKey {
if t.Cols[i].Name == t.PrimaryKey[j] {
cols = append(cols, t.Cols[i])
break
}
}
}
if len(cols) == 0 {
return nil, errz.Errorf("no primary key column(s) defined for table %q", t.Name)
}
return cols, nil
}
// SequenceCols returns the cols whose selector value is "../sequence()".
// In effect, this method returns the columns for whom a sequence value
// should be set during a db insert, similar to a db auto-increment column.
func (t *TableMapping) SequenceCols() []*ColMapping {
var cols []*ColMapping
for i := range t.Cols {
if t.Cols[i].Selector == "../sequence()" {
cols = append(cols, t.Cols[i])
}
}
return cols
}
// RequiredCols returns the cols that are required. This includes columns
// with explicit ColMapping.Required field as well as other columns such
// as those part of the primary key or sequence cols.
func (t *TableMapping) RequiredCols() []*ColMapping {
var cols []*ColMapping
pkCols, _ := t.PKCols()
seqCols := t.SequenceCols()
for _, col := range t.Cols {
col := col
switch {
case col.Required, colIndex(pkCols, col) >= 0, colIndex(seqCols, col) >= 0:
cols = append(cols, col)
default:
}
}
return cols
}
// ColMapping models a database table column.
type ColMapping struct {
// Name is the column name.
Name string `yaml:"col" json:"col"`
// Selector is an optional selector for the col value, e.g. "./guid/@isPermaLink" for an attribute of an XML element.
Selector string `yaml:"selector,omitempty" json:"selector,omitempty"`
// Kind is the data kind, e.g. "int", "text.
Kind kind.Kind `yaml:"kind" json:"kind"`
// Format is an optional type format for text values, e.g. "RFC3339" for a string.
Format string `yaml:"format,omitempty" json:"format,omitempty"`
// Charset is an optional charset for text values, e.g. "utf-8".
Charset string `yaml:"charset,omitempty" json:"charset,omitempty"`
// Foreign indicates that this column is a foreign key into a parent tbl.
Foreign string `yaml:"foreign,omitempty" json:"foreign,omitempty"`
// Unique is true if the column value is unique.
Unique bool `yaml:"unique,omitempty" json:"unique,omitempty"`
// Required is true if the column is required.
Required bool `yaml:"required" json:"required"`
// Comment is an optional column comment.
Comment string `yaml:"comment,omitempty" json:"comment,omitempty"`
}
func (c *ColMapping) String() string {
return stringz.SprintJSON(c)
}
// ValidateDriverDef checks that def is valid, returning one or
// more errors if not.
func ValidateDriverDef(def *DriverDef) []error {
drvrName, errs := validateDefRoot(def)
if len(errs) > 0 {
return errs
}
for i, tbl := range def.Tables {
tblName := fmt.Sprintf("%s.table[%d]", drvrName, i)
if tbl.Name == "" {
errs = append(errs, errz.Errorf("%s name is empty", tblName))
} else {
tblName = fmt.Sprintf("%s.table[%s]", drvrName, tbl.Name)
}
if tbl.Selector == "" {
errs = append(errs, errz.Errorf("%s selector is empty", tblName))
}
if len(tbl.Cols) == 0 {
errs = append(errs, errz.Errorf("%s cols is empty", tblName))
continue
}
if len(tbl.PrimaryKey) == 0 {
errs = append(errs, errz.Errorf("%s primary key must list at least one column", tblName))
} else {
for j, pkColName := range tbl.PrimaryKey {
if pkColName == "" {
errs = append(errs, errz.Errorf("%s primary key %d has empty name", tblName, j))
continue
}
// verify that the pk col exists in the cols
var foundIt bool
for k := range tbl.Cols {
if pkColName == tbl.Cols[k].Name {
foundIt = true
break
}
}
if !foundIt {
errs = append(errs,
errz.Errorf("%s specified primary key %q not found in cols", tblName, pkColName))
}
}
}
for j, col := range tbl.Cols {
colName := fmt.Sprintf("%s.col[%d]", tblName, j)
if col.Name == "" {
errs = append(errs, errz.Errorf("%s name is empty", colName))
} else {
colName = fmt.Sprintf("%s.col[%s]", tblName, col.Name)
}
// These kinds are nonsensical
switch col.Kind { //nolint:exhaustive
default:
case kind.Unknown, kind.Null:
errs = append(errs, errz.Errorf("%s.kind (%s) is invalid", colName, col.Kind))
}
}
}
return errs
}
func validateDefRoot(def *DriverDef) (drvrName string, errs []error) {
if def == nil {
// shouldn't happen
errs = append(errs, errz.New("def is nil"))
return "", errs
}
if def.Name == "" {
errs = append(errs, errz.New("driver name is empty"))
return "", errs
}
drvrName = fmt.Sprintf("driver[%s]", def.Name)
if def.Genre == "" {
errs = append(errs, errz.Errorf("%s.genre is empty", drvrName))
}
if def.Selector == "" {
errs = append(errs, errz.Errorf("%s.selector is empty", drvrName))
}
if def.Title == "" {
errs = append(errs, errz.Errorf("%s.title is empty", drvrName))
}
if len(def.Tables) == 0 {
errs = append(errs, errz.Errorf("%s.tables is empty", drvrName))
}
return drvrName, errs
}
// ToTableDef builds a TableDef from the TableMapping.
func ToTableDef(tblMapping *TableMapping) (*sqlmodel.TableDef, error) {
tblDef := &sqlmodel.TableDef{Name: tblMapping.Name}
colDefs := make([]*sqlmodel.ColDef, len(tblMapping.Cols))
pkCols, err := tblMapping.PKCols()
if err != nil {
return nil, err
}
tblDef.PKColName = pkCols[0].Name
for i, colMapping := range tblMapping.Cols {
colDef := &sqlmodel.ColDef{Table: tblDef, Name: colMapping.Name, Kind: colMapping.Kind}
colDefs[i] = colDef
}
tblDef.Cols = colDefs
return tblDef, nil
}
// NamesFromCols is a convenience function that returns a slice
// containing the name of each column.
func NamesFromCols(cols []*ColMapping) []string {
if cols == nil {
return nil
}
names := make([]string, len(cols))
for i := range cols {
names[i] = cols[i].Name
}
return names
}
// colIndex returns the index of needle in haystack, or -1.
func colIndex(haystack []*ColMapping, needle *ColMapping) int {
for i := range haystack {
if haystack[i] == needle {
return i
}
}
return -1
}
// Detector defines a document type detector.
type Detector struct {
// Type is the detector type, e.g. "suffix", "header", "scheme", etc.
Type string `yaml:"type" json:"type"`
// Key is the expected match for the detector's key field. E.g. "Content-Type". May be empty.
Key string `yaml:"key,omitempty" json:"key,omitempty"`
// Value is the expected match for the detector's value field. E.g. "application/rss+xml"
Value string `yaml:"value" json:"value"`
// Example is an example value that would match the detector, e.g. "Content-Type: application/rss+xml"
Example string `yaml:"example,omitempty" json:"example,omitempty"`
}
func (d *Detector) String() string {
return stringz.SprintJSON(d)
}