From 8b9e35311dd081874450fa09bb2bf41be3ef80f7 Mon Sep 17 00:00:00 2001 From: Isaac Corbrey Date: Sat, 24 Oct 2026 11:58:38 -0400 Subject: [PATCH] dovetail: Report problems in every file at once --- dovetail/default.nix | 75 ++++++++++++++++++++++++++++++-------------- dovetail/valibot.nix | 11 +++++-- 2 files changed, 60 insertions(+), 26 deletions(-) diff --git a/dovetail/default.nix b/dovetail/default.nix index dea5c5d..2918119 100644 --- a/dovetail/default.nix +++ b/dovetail/default.nix @@ -9,20 +9,40 @@ in { inputs, root }: let inherit (inputs) home-manager nixpkgs; - manifest = lib.importTOML (root + "/dovetail.toml") - |> v.parse schemas.root; + parseFile = schema: read: file: + v.safeParse schema (read (root + "/${file}")) + |> v.result.mapErr (lib.map (v.issue.inFile file)); - lock = + manifestResult = parseFile schemas.root lib.importTOML "dovetail.toml"; + + lockResult = if builtins.pathExists (root + "/dovetail.lock") - then lib.importJSON (root + "/dovetail.lock") |> v.parse topics.lock - else { topics = {}; }; + then parseFile topics.lock lib.importJSON "dovetail.lock" + else v.result.ok { topics = {}; }; - inRepo = key: root + "/${lib.removePrefix "./" key}"; + topicResults = builtins.readDir (root + "/topics") + |> lib.attrNames + |> lib.filter (lib.hasSuffix ".nix") + |> lib.map (file: lib.nameValuePair + (lib.removeSuffix ".nix" file) + (parseFile topics.record topics.authored "topics/${file}")) + |> lib.listToAttrs; - recipeManifests = manifest.recipes - |> lib.mapAttrs (key: _: - lib.importTOML (inRepo key + "/dovetail.toml") - |> v.parse schemas.recipe); + recipeResults = (manifestResult.ok.recipes or {}) + |> lib.mapAttrs (key: _: let + file = "${lib.removePrefix "./" key}/dovetail.toml"; + in + if builtins.pathExists (root + "/${file}") + then parseFile schemas.recipe lib.importTOML file + else v.result.err [ + (v.issue.at ["recipes" key] "has no ${file}" |> v.issue.inFile "dovetail.toml") + ]); + + manifest = manifestResult.ok; + lock = lockResult.ok; + recipeManifests = recipeResults |> lib.mapAttrs (_: recipe: recipe.ok); + + inRepo = key: root + "/${lib.removePrefix "./" key}"; ownTopicUri = name: "at://${manifest.did}/${topics.nsid}/${name}"; isOwnRecord = lib.hasPrefix "at://${manifest.did}/"; @@ -34,13 +54,8 @@ in { inputs, root }: let then ownTopicUri name else ref; - localTopics = builtins.readDir (root + "/topics") - |> lib.attrNames - |> lib.filter (lib.hasSuffix ".nix") - |> lib.map (file: lib.nameValuePair - (canonicalTopic "./topics/${lib.removeSuffix ".nix" file}") - (topics.authored (root + "/topics/${file}") |> v.parse topics.record)) - |> lib.listToAttrs; + localTopics = topicResults + |> lib.mapAttrs' (name: topic: lib.nameValuePair (canonicalTopic "./topics/${name}") topic.ok); pulledTopics = lock.topics |> lib.filterAttrs (uri: _: !(isOwnRecord uri)) @@ -53,11 +68,12 @@ in { inputs, root }: let unsourcedTopics = lock.topics |> lib.attrNames - |> lib.filter (uri: isOwnRecord uri && !(localTopics ? ${uri})); + |> lib.filter (uri: isOwnRecord uri && !(localTopics ? ${uri})) + |> lib.map (uri: + v.issue.at ["topics" uri] "is published, but topics/${baseNameOf uri}.nix doesn't exist" + |> v.issue.inFile "dovetail.lock"); - topicRecords = lib.throwIf (unsourcedTopics != []) - "dovetail.lock: published topics with no file under topics/: ${lib.concatStringsSep ", " unsourcedTopics}" - (localTopics // pulledTopics); + topicRecords = localTopics // pulledTopics; topicOptions = { options.topics = topicRecords |> lib.mapAttrs (_: topics.declare); @@ -135,7 +151,18 @@ in { inputs, root }: let in v.issue.at ["packages" use.key] "${ref.source} has no package ${v.issue.show (lib.concatStringsSep "." ref.attrPath)} for ${use.system}"); - manifestIssues = unknownSources ++ missingPackages ++ contributionIssues; + fileIssues = [manifestResult lockResult] + ++ lib.attrValues topicResults + ++ lib.attrValues recipeResults + |> lib.concatMap (outcome: outcome.err or []); + + manifestIssues = unknownSources ++ missingPackages ++ contributionIssues + |> lib.map (v.issue.inFile "dovetail.toml"); + + issues = + if fileIssues != [] + then fileIssues + else unsourcedTopics ++ manifestIssues; specificity = let mostSpecificFirst = ["user" "host" "package"]; @@ -199,8 +226,8 @@ in { inputs, root }: let modules = [identity topicOptions] ++ contributions ++ recipes; }; in { - homeConfigurations = lib.throwIf (manifestIssues != []) - "invalid dovetail.toml:${v.issue.format manifestIssues}" + homeConfigurations = lib.throwIf (issues != []) + "invalid configuration:${v.issue.format issues}" (manifest.hosts |> lib.concatMapAttrs (hostname: host: host.users |> lib.mapAttrs' (username: user: lib.nameValuePair "${username}@${hostname}" diff --git a/dovetail/valibot.nix b/dovetail/valibot.nix index 8ff70f0..03f6702 100644 --- a/dovetail/valibot.nix +++ b/dovetail/valibot.nix @@ -87,8 +87,15 @@ in lib.optionalString (nearest != []) "; did you mean ${lib.concatMapStringsSep " or " show nearest}?"; - format = lib.concatMapStrings - (reported: "\n - ${fmt.path reported.path}: ${reported.message}"); + inFile = file: reported: reported // { inherit file; }; + + format = issues: issues + |> lib.groupBy (reported: reported.file or "") + |> lib.mapAttrsToList (file: group: let + indent = if file == "" then " " else " "; + in lib.optionalString (file != "") "\n ${file}" + + lib.concatMapStrings (reported: "\n${indent}- ${fmt.path reported.path}: ${reported.message}") group) + |> lib.concatStrings; }; guard = let -- knotmirror 0.0.1