package fejkdata import ( "errors" "fmt" "strings" "github.com/larvit/fejkdata/internal/grammar" ) // resolvedRef is what a reference resolves to. head is "/" and the category's path from the root, // the key the category is held under; tail is the path read into the category. type resolvedRef struct { head string tail []string } // categoryOf is the category path a resolvedRef's head names. func categoryOf(head string) string { return strings.TrimPrefix(head, "/") } // refSegments resolves a reference written in folder to a path from the root. func refSegments(name string, folder []string) ([]string, error) { sigil, rest, err := grammar.RefShape(name) if err != nil { return nil, err } var base []string switch sigil { case ".": base = folder case "..": if len(folder) == 0 { return nil, fmt.Errorf("no folder above the root") } base = folder[:len(folder)-1] } segs, _ := grammar.SplitPath(rest) // grammar.RefShape proved it splits return append(append([]string{}, base...), segs...), nil } // resolveRefs resolves every reference t reads. func (t *template) resolveRefs(folder []string, label string, root map[string]node) (templateRefs, error) { var refs templateRefs names := refTokens(t.tokens) if len(names) == 0 { return refs, nil } refs.byName = make(map[string]resolvedRef, len(names)) refs.categories = make(map[string]node, len(names)) for _, name := range names { segments, err := refSegments(name, folder) if err != nil { return refs, fmt.Errorf("%s: reference {%s}: %w", label, name, err) } categorySegs, target, tail, err := resolveCategory(root, segments) if err != nil { return refs, fmt.Errorf("%s: reference {%s}: %w", label, name, err) } head := "/" + strings.Join(categorySegs, ".") if err := provePath(target, tail, head, 0); err != nil { return refs, fmt.Errorf("%s: reference {%s}: %w", label, name, err) } refs.categories[head] = target refs.byName[name] = resolvedRef{head, tail} } return refs, nil } // resolveCategory walks a dotted path through the folders to the category it // names, returning that category's segments, the node, and the tail left to read into it. func resolveCategory(root map[string]node, segments []string) (categorySegs []string, target node, tail []string, err error) { g, i := folderAt(&folder{children: root}, segments) switch { case i == len(segments): return nil, nil, nil, fmt.Errorf("names a folder, not a value") case grammar.IsSelector(segments[i]): return nil, nil, nil, fmt.Errorf("%s is a folder, not a table, so it has no row to select", strings.Join(segments[:i], ".")) } n, ok := g.children[segments[i]] if !ok { return nil, nil, nil, noEntry{segments[i]} } return segments[:i+1], n, segments[i+1:], nil } // folderAt is the deepest folder a path from root walks into, and the index of the first // segment naming no folder in it, len(segs) where every segment does. func folderAt(root *folder, segs []string) (*folder, int) { g := root for i, seg := range segs { sub, isFolder := g.children[seg].(*folder) if !isFolder { return g, i } g = sub } return g, len(segs) } // refTokens returns the reference names a format reads, as tokens or as operands, or binds. func refTokens(toks []grammar.Token) []string { var refs []string seen := map[string]bool{} for _, tok := range toks { names := tokenReads(tok) if tok.Kind == grammar.NameBind { names = []string{tok.BoundRef} } for _, name := range names { if grammar.IsRef(name) && !seen[name] { seen[name] = true refs = append(refs, name) } } } return refs } // noEntry is a path segment naming nothing in the folder it reads. type noEntry struct{ seg string } func (e noEntry) Error() string { return fmt.Sprintf("no entry %q", e.seg) } // withMissingModules adds to err, where a path names nothing, each namespace of reads that // nothing in root provides, as the modules a loaded manifest reads and none loaded; or else // that no loaded manifest names a module to add, the mistake being the path's own. func withMissingModules(err error, root *folder, reads []string) error { var ne noEntry if !errors.As(err, &ne) { return err } var missing []string for _, namespace := range reads { segs := strings.Split(namespace, ".") if _, i := folderAt(root, segs); i < len(segs) { missing = append(missing, namespace) } } if len(missing) == 0 { return fmt.Errorf("%w, and no loaded manifest names a module to add", err) } return fmt.Errorf("%w; is a module missing? The loaded modules read %s, which nothing loaded provides", err, strings.Join(missing, ", ")) } // noDataNote adds to err, where a segment names nothing and f holds no data at all, that no // data is loaded. func (f *Generator) noDataNote(err error) error { var ne noEntry if errors.As(err, &ne) && len(f.root.children) == 0 && len(f.root.unloaded) == 0 { return fmt.Errorf("%w; no data is loaded", err) } return err }