{ lib, cfg, models, allPhones }: # Provisioned sharedPhones require a MAC address key (lib.mapAttrsToList (key: phone: { assertion = !models.${phone.model}.hasProvisioning || builtins.match "[0-9a-f]{12}" key != null; message = "services.voip: sharedPhone \"${key}\" (model ${phone.model}) requires a lowercase 12-char hex MAC address as key"; }) cfg.sharedPhones) ++ # Provisioned sharedPhones require a non-empty label (lib.mapAttrsToList (key: phone: { assertion = !models.${phone.model}.hasProvisioning || phone.label != ""; message = "services.voip: sharedPhone \"${key}\" (model ${phone.model}) requires a non-empty label"; }) cfg.sharedPhones) ++ # Provisioned person phones require a MAC address key (lib.concatLists (lib.mapAttrsToList (personKey: person: lib.mapAttrsToList (key: phone: { assertion = !models.${phone.model}.hasProvisioning || builtins.match "[0-9a-f]{12}" key != null; message = "services.voip: persons.${personKey}.phones.\"${key}\" (model ${phone.model}) requires a lowercase 12-char hex MAC address as key"; }) person.phones ) cfg.persons)) ++ # Provisioned person phones require a non-empty label (lib.concatLists (lib.mapAttrsToList (personKey: person: lib.mapAttrsToList (key: phone: { assertion = !models.${phone.model}.hasProvisioning || phone.label != ""; message = "services.voip: persons.${personKey}.phones.\"${key}\" (model ${phone.model}) requires a non-empty label"; }) person.phones ) cfg.persons)) ++ # No duplicate phone keys across sharedPhones and persons.*.phones [{ assertion = let keys = lib.attrNames allPhones; in lib.length keys == lib.length (lib.unique keys); message = "services.voip: duplicate phone key detected across sharedPhones and persons.*.phones"; }] ++ # No duplicate extensions across sharedPhones and persons [{ assertion = let exts = (lib.mapAttrsToList (_: p: p.extension) cfg.sharedPhones) ++ (lib.mapAttrsToList (_: p: p.extension) cfg.persons); in lib.length exts == lib.length (lib.unique exts); message = "services.voip: duplicate extension number across sharedPhones and persons"; }] ++ # dids require at least one sipTrunk (lib.optionals (cfg.dids != {}) [{ assertion = cfg.sipTrunks != {}; message = "services.voip: dids are configured but sipTrunks is empty"; }]) ++ # each DID must reference an existing trunk (lib.mapAttrsToList (did: didCfg: { assertion = lib.hasAttr didCfg.trunk cfg.sipTrunks; message = "services.voip: DID ${did} references trunk \"${didCfg.trunk}\" which is not in services.voip.sipTrunks"; }) cfg.dids) ++ # dids with mailbox="shared" require sharedMailbox (lib.mapAttrsToList (did: didCfg: { assertion = didCfg.mailbox != "shared" || cfg.sharedMailbox != null; message = "services.voip: DID ${did} has mailbox=\"shared\" but sharedMailbox is not configured"; }) cfg.dids) ++ # dids with mailbox="person" require routing.type="person" (lib.mapAttrsToList (did: didCfg: { assertion = didCfg.mailbox != "person" || didCfg.routing.type == "person"; message = "services.voip: DID ${did} has mailbox=\"person\" but routing.type is not \"person\""; }) cfg.dids) ++ # dids routing.type="person" — person key must be non-empty (lib.mapAttrsToList (did: didCfg: { assertion = didCfg.routing.type != "person" || didCfg.routing.person != ""; message = "services.voip: DID ${did} has routing.type=\"person\" but routing.person is not set"; }) cfg.dids) ++ # dids routing.type="person" — referenced person must exist (lib.mapAttrsToList (did: didCfg: { assertion = didCfg.routing.type != "person" || didCfg.routing.person == "" || lib.hasAttr didCfg.routing.person cfg.persons; message = "services.voip: DID ${did} references person \"${didCfg.routing.person}\" which is not in services.voip.persons"; }) cfg.dids) ++ # dids routing.type="persons" — persons list must be non-empty (lib.mapAttrsToList (did: didCfg: { assertion = didCfg.routing.type != "persons" || didCfg.routing.persons != []; message = "services.voip: DID ${did} has routing.type=\"persons\" but routing.persons is empty"; }) cfg.dids) ++ # dids routing.type="persons" — all referenced persons must exist (lib.concatLists (lib.mapAttrsToList (did: didCfg: lib.optionals (didCfg.routing.type == "persons") (map (p: { assertion = lib.hasAttr p cfg.persons; message = "services.voip: DID ${did} references person \"${p}\" which is not in services.voip.persons"; }) didCfg.routing.persons) ) cfg.dids)) ++ # dids musicOnHold must reference an existing mohClass (lib.concatLists (lib.mapAttrsToList (did: didCfg: lib.optional (didCfg.musicOnHold != null) { assertion = lib.hasAttr didCfg.musicOnHold cfg.mohClasses; message = "services.voip: DID ${did} references mohClass \"${didCfg.musicOnHold}\" which is not in services.voip.mohClasses"; } ) cfg.dids)) ++ # sipTrunks: each required field needs either a literal or a file (lib.concatLists (lib.mapAttrsToList (name: t: [ { assertion = t.host != "" || t.hostFile != null; message = "services.voip: sipTrunks.\"${name}\" requires host or hostFile"; } { assertion = t.username != "" || t.usernameFile != null; message = "services.voip: sipTrunks.\"${name}\" requires username or usernameFile"; } { assertion = t.password != "" || t.passwordFile != null; message = "services.voip: sipTrunks.\"${name}\" requires password or passwordFile"; } ]) cfg.sipTrunks)) ++ # dids: each DID needs a number either inline or from a file (lib.mapAttrsToList (id: d: { assertion = d.number != "" || d.numberFile != null; message = "services.voip: dids.\"${id}\" requires number or numberFile"; }) cfg.dids) ++ # extensions with mode="app" must have a non-null app field (lib.mapAttrsToList (ext: extCfg: { assertion = extCfg.mode != "app" || extCfg.app != null; message = "services.voip: extension \"${ext}\" has mode=\"app\" but app is not set"; }) cfg.extensions) ++ # intercomPrefix must not collide with any declared extension (lib.optionals (cfg.intercomPrefix != null) (lib.mapAttrsToList (_key: phone: let ext = "${cfg.intercomPrefix}${phone.extension}"; in { assertion = !lib.hasAttr ext cfg.extensions; message = "services.voip: auto-generated intercom extension \"${ext}\" collides with a declared extension; rename it or change intercomPrefix"; } ) allPhones))