-
-
Notifications
You must be signed in to change notification settings - Fork 18.4k
Add types.attrTag
#284551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add types.attrTag
#284551
Changes from all commits
ca81a89
0feea2d
2ceb555
1ad3077
6949bc2
5b49672
42d3b54
0bc9783
e090bb5
4c7d990
2e1d470
475a55b
c0f54d3
fa8b46a
bcd7746
1465777
47e4a18
2d791b5
f354686
74831d8
cf4968a
22d7f14
35fe538
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /* | ||
| A basic documentation generating module. | ||
| Declares and defines a `docs` option, suitable for making assertions about | ||
| the extraction "phase" of documentation generation. | ||
| */ | ||
| { lib, options, ... }: | ||
|
|
||
| let | ||
| inherit (lib) | ||
| head | ||
| length | ||
| mkOption | ||
| types | ||
| ; | ||
|
|
||
| traceListSeq = l: v: lib.foldl' (a: b: lib.traceSeq b a) v l; | ||
|
|
||
| in | ||
|
|
||
| { | ||
| options.docs = mkOption { | ||
| type = types.lazyAttrsOf types.raw; | ||
| description = '' | ||
| All options to be rendered, without any visibility filtering applied. | ||
| ''; | ||
| }; | ||
| config.docs = | ||
| lib.zipAttrsWith | ||
| (name: values: | ||
| if length values > 1 then | ||
| traceListSeq values | ||
| abort "Multiple options with the same name: ${name}" | ||
| else | ||
| assert length values == 1; | ||
| head values | ||
| ) | ||
| (map | ||
| (opt: { ${opt.name} = opt; }) | ||
| (lib.optionAttrSetToDocList options) | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { lib, ... }: | ||
| let | ||
| inherit (lib) types mkOption; | ||
| in | ||
| { | ||
| options = { | ||
| opt = mkOption { | ||
| type = types.attrTag { | ||
| int = types.int; | ||
| }; | ||
| default = { int = 1; }; | ||
| }; | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| { lib, config, options, ... }: | ||
| let | ||
| inherit (lib) mkOption types; | ||
| forceDeep = x: builtins.deepSeq x x; | ||
| mergedSubOption = (options.merged.type.getSubOptions options.merged.loc).extensible."merged.<name>"; | ||
| in | ||
| { | ||
| options = { | ||
| intStrings = mkOption { | ||
| type = types.attrsOf | ||
| (types.attrTag { | ||
| left = mkOption { | ||
| type = types.int; | ||
| }; | ||
| right = mkOption { | ||
| type = types.str; | ||
| }; | ||
| }); | ||
| }; | ||
| nested = mkOption { | ||
| type = types.attrTag { | ||
| left = mkOption { | ||
| type = types.int; | ||
| }; | ||
| right = mkOption { | ||
| type = types.attrTag { | ||
| left = mkOption { | ||
| type = types.int; | ||
| }; | ||
| right = mkOption { | ||
| type = types.str; | ||
| }; | ||
| }; | ||
| }; | ||
| }; | ||
| }; | ||
| merged = mkOption { | ||
| type = types.attrsOf ( | ||
| types.attrTag { | ||
| yay = mkOption { | ||
| type = types.int; | ||
| }; | ||
| extensible = mkOption { | ||
| type = types.enum [ "foo" ]; | ||
| }; | ||
| } | ||
| ); | ||
| }; | ||
| submodules = mkOption { | ||
| type = types.attrsOf ( | ||
| types.attrTag { | ||
| foo = mkOption { | ||
| type = types.submodule { | ||
| options = { | ||
| bar = mkOption { | ||
| type = types.int; | ||
| }; | ||
| }; | ||
| }; | ||
| }; | ||
| qux = mkOption { | ||
| type = types.str; | ||
| description = "A qux for when you don't want a foo"; | ||
| }; | ||
| } | ||
| ); | ||
| }; | ||
| okChecks = mkOption {}; | ||
| }; | ||
| imports = [ | ||
| ./docs.nix | ||
| { | ||
| options.merged = mkOption { | ||
| type = types.attrsOf ( | ||
| types.attrTag { | ||
| nay = mkOption { | ||
| type = types.bool; | ||
| }; | ||
| extensible = mkOption { | ||
| type = types.enum [ "bar" ]; | ||
| }; | ||
| } | ||
| ); | ||
| }; | ||
| } | ||
| ]; | ||
| config = { | ||
| intStrings.syntaxError = 1; | ||
| intStrings.syntaxError2 = {}; | ||
| intStrings.syntaxError3 = { a = true; b = true; }; | ||
| intStrings.syntaxError4 = lib.mkMerge [ { a = true; } { b = true; } ]; | ||
| intStrings.mergeError = lib.mkMerge [ { int = throw "do not eval"; } { string = throw "do not eval"; } ]; | ||
| intStrings.badTagError.rite = throw "do not eval"; | ||
| intStrings.badTagTypeError.left = "bad"; | ||
| intStrings.numberOne.left = 1; | ||
| intStrings.hello.right = "hello world"; | ||
| nested.right.left = "not a number"; | ||
| merged.negative.nay = false; | ||
| merged.positive.yay = 100; | ||
| merged.extensi-foo.extensible = "foo"; | ||
| merged.extensi-bar.extensible = "bar"; | ||
| okChecks = builtins.addErrorContext "while evaluating the assertions" ( | ||
| assert config.intStrings.hello == { right = "hello world"; }; | ||
| assert config.intStrings.numberOne == { left = 1; }; | ||
| assert config.merged.negative == { nay = false; }; | ||
| assert config.merged.positive == { yay = 100; }; | ||
| assert config.merged.extensi-foo == { extensible = "foo"; }; | ||
| assert config.merged.extensi-bar == { extensible = "bar"; }; | ||
| assert config.docs."submodules.<name>.foo.bar".type == "signed integer"; | ||
| assert config.docs."submodules.<name>.qux".type == "string"; | ||
| assert config.docs."submodules.<name>.qux".declarations == [ __curPos.file ]; | ||
| assert config.docs."submodules.<name>.qux".loc == [ "submodules" "<name>" "qux" ]; | ||
| assert config.docs."submodules.<name>.qux".name == "submodules.<name>.qux"; | ||
| assert config.docs."submodules.<name>.qux".description == "A qux for when you don't want a foo"; | ||
| assert config.docs."submodules.<name>.qux".readOnly == false; | ||
| assert config.docs."submodules.<name>.qux".visible == true; | ||
| # Not available (yet?) | ||
| # assert config.docs."submodules.<name>.qux".declarationsWithPositions == [ ... ]; | ||
| assert options.submodules.declarations == [ __curPos.file ]; | ||
| assert lib.length options.submodules.declarationPositions == 1; | ||
| assert (lib.head options.submodules.declarationPositions).file == __curPos.file; | ||
| assert options.merged.declarations == [ __curPos.file __curPos.file ]; | ||
| assert lib.length options.merged.declarationPositions == 2; | ||
| assert (lib.elemAt options.merged.declarationPositions 0).file == __curPos.file; | ||
| assert (lib.elemAt options.merged.declarationPositions 1).file == __curPos.file; | ||
| assert (lib.elemAt options.merged.declarationPositions 0).line != (lib.elemAt options.merged.declarationPositions 1).line; | ||
| assert mergedSubOption.declarations == [ __curPos.file __curPos.file ]; | ||
| assert lib.length mergedSubOption.declarationPositions == 2; | ||
| assert (lib.elemAt mergedSubOption.declarationPositions 0).file == __curPos.file; | ||
| assert (lib.elemAt mergedSubOption.declarationPositions 1).file == __curPos.file; | ||
| assert (lib.elemAt mergedSubOption.declarationPositions 0).line != (lib.elemAt mergedSubOption.declarationPositions 1).line; | ||
| assert lib.length config.docs."merged.<name>.extensible".declarations == 2; | ||
| true); | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ let | |
| isList | ||
| isString | ||
| isStorePath | ||
| throwIf | ||
| toDerivation | ||
| toList | ||
| ; | ||
|
|
@@ -65,6 +66,11 @@ let | |
| fixupOptionType | ||
| mergeOptionDecls | ||
| ; | ||
|
|
||
| inAttrPosSuffix = v: name: | ||
| let pos = builtins.unsafeGetAttrPos name v; in | ||
| if pos == null then "" else " at ${pos.file}:${toString pos.line}:${toString pos.column}"; | ||
|
|
||
| outer_types = | ||
| rec { | ||
| __attrsFailEvaluation = true; | ||
|
|
@@ -152,7 +158,7 @@ rec { | |
| # If it doesn't, this should be {} | ||
| # This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`. | ||
| emptyValue ? {} | ||
| , # Return a flat list of sub-options. Used to generate | ||
| , # Return a flat attrset of sub-options. Used to generate | ||
| # documentation. | ||
| getSubOptions ? prefix: {} | ||
| , # List of modules if any, or null if none. | ||
|
|
@@ -614,6 +620,100 @@ rec { | |
| nestedTypes.elemType = elemType; | ||
| }; | ||
|
|
||
| attrTag = tags: | ||
| let tags_ = tags; in | ||
| let | ||
| tags = | ||
| mapAttrs | ||
| (n: opt: | ||
| builtins.addErrorContext "while checking that attrTag tag ${lib.strings.escapeNixIdentifier n} is an option with a type${inAttrPosSuffix tags_ n}" ( | ||
| throwIf (opt._type or null != "option") | ||
| "In attrTag, each tag value must be an option, but tag ${lib.strings.escapeNixIdentifier n} ${ | ||
| if opt?_type then | ||
| if opt._type == "option-type" | ||
| then "was a bare type, not wrapped in mkOption." | ||
| else "was of type ${lib.strings.escapeNixString opt._type}." | ||
| else "was not."}" | ||
| opt // { | ||
| declarations = opt.declarations or ( | ||
| let pos = builtins.unsafeGetAttrPos n tags_; | ||
| in if pos == null then [] else [ pos.file ] | ||
| ); | ||
| declarationPositions = opt.declarationPositions or ( | ||
| let pos = builtins.unsafeGetAttrPos n tags_; | ||
| in if pos == null then [] else [ pos ] | ||
| ); | ||
| } | ||
| )) | ||
| tags_; | ||
| choicesStr = concatMapStringsSep ", " lib.strings.escapeNixIdentifier (attrNames tags); | ||
| in | ||
| mkOptionType { | ||
| name = "attrTag"; | ||
| description = "attribute-tagged union"; | ||
| descriptionClass = "noun"; | ||
| getSubOptions = prefix: | ||
| mapAttrs | ||
| (tagName: tagOption: { | ||
| "${lib.showOption prefix}" = | ||
| tagOption // { | ||
| loc = prefix ++ [ tagName ]; | ||
| }; | ||
| }) | ||
| tags; | ||
| check = v: isAttrs v && length (attrNames v) == 1 && tags?${head (attrNames v)}; | ||
| merge = loc: defs: | ||
| let | ||
| choice = head (attrNames (head defs).value); | ||
| checkedValueDefs = map | ||
| (def: | ||
| assert (length (attrNames def.value)) == 1; | ||
| if (head (attrNames def.value)) != choice | ||
| then throw "The option `${showOption loc}` is defined both as `${choice}` and `${head (attrNames def.value)}`, in ${showFiles (getFiles defs)}." | ||
| else { inherit (def) file; value = def.value.${choice}; }) | ||
| defs; | ||
| in | ||
| if tags?${choice} | ||
| then | ||
| { ${choice} = | ||
| (lib.modules.evalOptionValue | ||
| (loc ++ [choice]) | ||
| tags.${choice} | ||
| checkedValueDefs | ||
| ).value; | ||
| } | ||
| else throw "The option `${showOption loc}` is defined as ${lib.strings.escapeNixIdentifier choice}, but ${lib.strings.escapeNixIdentifier choice} is not among the valid choices (${choicesStr}). Value ${choice} was defined in ${showFiles (getFiles defs)}."; | ||
| nestedTypes = tags; | ||
| functor = defaultFunctor "attrTag" // { | ||
| type = { tags, ... }: types.attrTag tags; | ||
| payload = { inherit tags; }; | ||
| binOp = | ||
|
||
| let | ||
| # Add metadata in the format that submodules work with | ||
| wrapOptionDecl = | ||
| option: { options = option; _file = "<attrTag {...}>"; pos = null; }; | ||
| in | ||
| a: b: { | ||
| tags = a.tags // b.tags // | ||
| mapAttrs | ||
| (tagName: bOpt: | ||
| lib.mergeOptionDecls | ||
| # FIXME: loc is not accurate; should include prefix | ||
| # Fortunately, it's only used for error messages, where a "relative" location is kinda ok. | ||
| # It is also returned though, but use of the attribute seems rare? | ||
| [tagName] | ||
| [ (wrapOptionDecl a.tags.${tagName}) (wrapOptionDecl bOpt) ] | ||
| // { | ||
| # mergeOptionDecls is not idempotent in these attrs: | ||
| declarations = a.tags.${tagName}.declarations ++ bOpt.declarations; | ||
| declarationPositions = a.tags.${tagName}.declarationPositions ++ bOpt.declarationPositions; | ||
| } | ||
| ) | ||
| (builtins.intersectAttrs a.tags b.tags); | ||
| }; | ||
| }; | ||
| }; | ||
|
|
||
| uniq = unique { message = ""; }; | ||
|
|
||
| unique = { message }: type: mkOptionType rec { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.