package dbml import ( "fmt" "regexp" "strings" "git.warky.dev/wdevs/relspecgo/pkg/models" ) // directiveLineRegex matches a dialect directive line: // // @postgres: partition by RANGE (created_at) // @postgres(id): identity always // // Group 1 is the namespace, group 2 the optional (column) target, group 3 the // raw argument text (validated separately so error messages can be specific). var directiveLineRegex = regexp.MustCompile(`^@([^():]*)(?:\(([^()]*)\))?\s*:(.*)$`) // namespaceRegex is the grammar for a directive namespace. var namespaceRegex = regexp.MustCompile(`^[a-z][a-z0-9_]*$`) // parsedDirective is a directive line that has been parsed but not yet attached // to a model object. type parsedDirective struct { namespace string target string // column name; "" when absent args string line int } // parseDirectiveLine parses a single "@namespace[(target)]: args" line. func parseDirectiveLine(line string, lineNo int) (parsedDirective, error) { m := directiveLineRegex.FindStringSubmatch(line) if m == nil { return parsedDirective{}, fmt.Errorf( "dbml: line %d: malformed directive %q (expected \"@namespace: args\")", lineNo, line) } ns := strings.TrimSpace(m[1]) target := strings.TrimSpace(m[2]) args := strings.TrimSpace(m[3]) if !namespaceRegex.MatchString(ns) { return parsedDirective{}, fmt.Errorf( "dbml: line %d: invalid directive namespace %q (must match [a-z][a-z0-9_]*)", lineNo, ns) } if args == "" { return parsedDirective{}, fmt.Errorf("dbml: line %d: directive @%s has no arguments", lineNo, ns) } if target != "" { target = stripQuotes(target) } return parsedDirective{namespace: ns, target: target, args: args, line: lineNo}, nil } // attachDirective resolves the target model object from the current parser state // and stores the directive in its Metadata, enforcing location, duplicate and // strict-mode rules. func (r *Reader) attachDirective( pd parsedDirective, db *models.Database, table *models.Table, inTable, inIndexes bool, lastIndex *models.Index, ) error { strict := r.options != nil && r.options.StrictDirectives key := models.DirectiveKey(pd.args) var meta map[string]any var location string switch { case inIndexes: if pd.target != "" { return fmt.Errorf("dbml: line %d: directive target (%s) is not allowed inside an indexes block", pd.line, pd.target) } if lastIndex == nil { return fmt.Errorf("dbml: line %d: directive @%s must follow an index definition", pd.line, pd.namespace) } if lastIndex.Metadata == nil { lastIndex.Metadata = make(map[string]any) } meta = lastIndex.Metadata location = models.DirectiveLocationIndex case inTable && table != nil: if pd.target != "" { col, ok := table.Columns[pd.target] if !ok { return fmt.Errorf("dbml: line %d: directive target column %q not found in table %q", pd.line, pd.target, table.Name) } if col.Metadata == nil { col.Metadata = make(map[string]any) } meta = col.Metadata location = models.DirectiveLocationColumn } else { if table.Metadata == nil { table.Metadata = make(map[string]any) } meta = table.Metadata location = models.DirectiveLocationTable } default: if pd.target != "" { return fmt.Errorf("dbml: line %d: directive target (%s) is only valid inside a table", pd.line, pd.target) } if db.Metadata == nil { db.Metadata = make(map[string]any) } meta = db.Metadata location = models.DirectiveLocationDatabase } spec, documented := models.LookupDirectiveSpec(pd.namespace, key) if strict && !documented { return fmt.Errorf("dbml: line %d: unknown directive @%s: %s (strict mode)", pd.line, pd.namespace, key) } if documented && !models.DirectiveLocationAllowed(pd.namespace, key, location) { return fmt.Errorf("dbml: line %d: directive @%s: %s is not valid at %s level", pd.line, pd.namespace, key, location) } if documented && spec.Singleton && models.HasDirective(meta, pd.namespace, key) { return fmt.Errorf("dbml: line %d: duplicate @%s directive %q at %s level", pd.line, pd.namespace, key, location) } models.AddDirective(meta, models.Directive{ Namespace: pd.namespace, Key: key, Args: pd.args, Line: pd.line, }) return nil }