package gsp import ( "encoding/json" "fmt" "sort" ) func (p *Project) Validate(stage string) Report { report := Report{OK: true} for _, issue := range p.LoadIssues { report.Errors = append(report.Errors, issue) report.OK = false } for _, unit := range p.Units { if unit.ID == "" { report.addError("missing_id", "", unit.File, "GSP requires id") } if unit.Resolution != "" { if _, ok := resolutionValue(unit.Resolution); !ok { report.addError("invalid_resolution", unit.ID, unit.File, fmt.Sprintf("resolution %q is not allowed", unit.Resolution)) } } for _, rel := range unit.With { if _, ok := p.ByID[rel.ID]; !ok { report.addError("missing_with", unit.ID, unit.File, fmt.Sprintf("with references missing GSP %q", rel.ID)) } } if unit.Refines != "" { if _, ok := p.ByID[unit.Refines]; !ok { report.addError("missing_refines", unit.ID, unit.File, fmt.Sprintf("refines references missing GSP %q", unit.Refines)) } } if unit.Context == "" { report.addNotice("placeholder", unit.ID, unit.File, "GSP has no context and is treated as placeholder") } } for id, units := range p.Duplicates { files := make([]string, 0, len(units)) seen := map[string]bool{} for _, unit := range units { if !seen[unit.File] { files = append(files, unit.File) seen[unit.File] = true } } sort.Strings(files) report.addError("duplicate_id", id, "", fmt.Sprintf("id %q is defined more than once: %v", id, files)) } if stage != "" { stageReport := p.StageCheck(stage) report.Errors = append(report.Errors, stageReport.Errors...) report.Warnings = append(report.Warnings, stageReport.Warnings...) if len(stageReport.Errors) > 0 { report.OK = false } } return report } func (p *Project) Index() []IndexEntry { entries := make([]IndexEntry, 0, len(p.Units)) for _, unit := range p.Units { with := make([]string, 0, len(unit.With)) for _, rel := range unit.With { with = append(with, rel.ID) } sort.Strings(with) entries = append(entries, IndexEntry{ ID: unit.ID, File: unit.File, Type: unit.Type, Resolution: unit.Resolution, With: with, Refines: unit.Refines, }) } sort.Slice(entries, func(i, j int) bool { return entries[i].ID < entries[j].ID }) return entries } func (p *Project) StageCheck(stage string) Report { report := Report{OK: true} required, ok := map[string]string{ "design": "L0", "integrate": "L2", "implement": "L3", "bind": "L4", "release": "L5", }[stage] if !ok { report.addError("unknown_stage", "", "", fmt.Sprintf("unknown stage %q", stage)) return report } minRank, _ := resolutionValue(required) for _, unit := range p.Units { rank, ok := resolutionValue(unit.Resolution) if !ok { report.addError("invalid_resolution", unit.ID, unit.File, fmt.Sprintf("resolution %q is not allowed", unit.Resolution)) continue } if rank < minRank { report.addError("low_resolution", unit.ID, unit.File, fmt.Sprintf("resolution %s is below %s for stage %s", displayResolution(unit.Resolution), required, stage)) } } if len(report.Errors) == 0 { report.OK = true } return report } func displayResolution(value string) string { if value == "" { return "L0" } return value } func (p *Project) Trace(id string, depth int, filter Filter) TraceResult { flatten := p.Flatten(id, depth, filter) graph := p.graphForUnits(flatten.Units) return TraceResult{ Entry: id, Depth: depth, Nodes: flatten.Units, Edges: graph.Edges, Warnings: flatten.Warnings, } } func (p *Project) Flatten(id string, depth int, filter Filter) FlattenResult { walker := &walker{ project: p, depth: depth, filter: filter, seen: map[string]bool{}, stack: map[string]bool{}, } walker.visit(id, 0) return FlattenResult{ Entry: id, Depth: depth, Units: walker.units, Warnings: walker.warnings, } } func (p *Project) Pack(id string, depth, budget int, filter Filter) PackResult { flattened := p.Flatten(id, depth, filter) units := make([]*Unit, 0, len(flattened.Units)) approx := 0 truncated := false for _, unit := range flattened.Units { candidate := append(units, unit) data, _ := json.Marshal(candidate) if budget > 0 && len(data) > budget && len(units) > 0 { truncated = true break } units = candidate approx = len(data) } return PackResult{ Entry: id, Depth: depth, Budget: budget, Truncated: truncated, Units: units, ApproxChars: approx, Warnings: flattened.Warnings, } } type walker struct { project *Project depth int filter Filter seen map[string]bool stack map[string]bool units []*Unit warnings []Issue } func (w *walker) visit(id string, currentDepth int) { if w.depth >= 0 && currentDepth > w.depth { return } if w.stack[id] { w.warnings = append(w.warnings, Issue{Level: "warning", Code: "cycle", ID: id, Message: fmt.Sprintf("cycle detected at %q", id)}) return } unit, ok := w.project.ByID[id] if !ok { w.warnings = append(w.warnings, Issue{Level: "warning", Code: "missing", ID: id, Message: fmt.Sprintf("missing GSP %q", id)}) return } if w.seen[id] { return } w.seen[id] = true if currentDepth == 0 || w.filter.Allows(unit) { w.units = append(w.units, unit) } w.stack[id] = true if unit.Refines != "" { w.visit(unit.Refines, currentDepth+1) } withIDs := make([]string, 0, len(unit.With)) for _, rel := range unit.With { withIDs = append(withIDs, rel.ID) } sort.Strings(withIDs) for _, relID := range withIDs { w.visit(relID, currentDepth+1) } delete(w.stack, id) }