mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-15 21:32:26 +01:00
feat(nixgen): initial implementation of schemars-nixgen
This crate converts JSON Schema (as generated by schemars) into strongly-typed Nix option definitions for the NixOS module system. The primary motivation was to generate NixOS module options for komorebi and komorebi-bar configurations, allowing users to write type-checked Nix configurations that map directly to the JSON config files expected by those applications. Some high level notes on the implementation: * Handles all common JSON Schema patterns including primitives, arrays, objects, enums, refs, oneOf/anyOf/allOf, and nullable types * Detects and correctly generates externally-tagged unions (Rust's default serde enum representation) as submodules with optional fields * Detects internally-tagged unions and mixed enum+object unions * Generates let bindings for frequently-used type definitions to avoid repetition in the output (e.g., large enums like Base16Value) * Uses topological sorting to generate type bindings in dependency order, ensuring nested refs are deduplicated correctly * Handles patternProperties for index-based configuration patterns
This commit is contained in:
commit
7763d679a5
14 files changed
+3803
No files matched your search
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
/result
|
||||
/.direnv
|
||||
Generated
+676
@@ -0,0 +1,676 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b"
|
||||
dependencies = [
|
||||
"gimli",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6"
|
||||
dependencies = [
|
||||
"addr2line",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"miniz_oxide",
|
||||
"object",
|
||||
"rustc-demangle",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.53"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"terminal_size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||
|
||||
[[package]]
|
||||
name = "color-eyre"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"color-spantrace",
|
||||
"eyre",
|
||||
"indenter",
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-spantrace"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"owo-colors",
|
||||
"tracing-core",
|
||||
"tracing-error",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "dyn-clone"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec"
|
||||
dependencies = [
|
||||
"indenter",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.32.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indenter"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.178"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
|
||||
dependencies = [
|
||||
"ref-cast-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ref-cast-impl"
|
||||
version = "1.0.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea"
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars-nixgen"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"color-eyre",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json_lenient",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301858a4023d78debd2353c7426dc486001bddc91ae31a76fb1f55132f7e2633"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.29.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.147"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json_lenient"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e033097bf0d2b59a62b42c18ebbb797503839b26afdda2c4e1415cb6c813540"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-error"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db"
|
||||
dependencies = [
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||
dependencies = [
|
||||
"sharded-slab",
|
||||
"thread_local",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dccf46b25b205e4bebe1d5258a991df1cc17801017a845cb5b3fe0269781aa"
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "schemars-nixgen"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license-file = "LICENSE.md"
|
||||
|
||||
[lib]
|
||||
name = "schemars_nixgen"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "schemars-nixgen"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive", "wrap_help"] }
|
||||
color-eyre = "0.6"
|
||||
schemars = "1.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { package = "serde_json_lenient", version = "0.2" }
|
||||
thiserror = "2"
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
# Komorebi License
|
||||
|
||||
Version 2.0.0
|
||||
|
||||
## Acceptance
|
||||
|
||||
In order to get any license under these terms, you must agree
|
||||
to them as both strict obligations and conditions to all
|
||||
your licenses.
|
||||
|
||||
## Copyright License
|
||||
|
||||
The licensor grants you a copyright license for the software
|
||||
to do everything you might do with the software that would
|
||||
otherwise infringe the licensor's copyright in it for any
|
||||
permitted purpose. However, you may only distribute the source
|
||||
code of the software according to the [Distribution License](
|
||||
#distribution-license), you may only make changes according
|
||||
to the [Changes License](#changes-license), and you may not
|
||||
otherwise distribute the software or new works based on the
|
||||
software.
|
||||
|
||||
## Distribution License
|
||||
|
||||
The licensor grants you an additional copyright license to
|
||||
distribute copies of the source code of the software. Your
|
||||
license to distribute covers distributing the source code of
|
||||
the software with changes permitted by the [Changes License](
|
||||
#changes-license).
|
||||
|
||||
## Changes License
|
||||
|
||||
The licensor grants you an additional copyright license to
|
||||
make changes for any permitted purpose.
|
||||
|
||||
## Patent License
|
||||
|
||||
The licensor grants you a patent license for the software that
|
||||
covers patent claims the licensor can license, or becomes able
|
||||
to license, that you would infringe by using the software.
|
||||
|
||||
## Personal Uses
|
||||
|
||||
Personal use for research, experiment, and testing for
|
||||
the benefit of public knowledge, personal study, private
|
||||
entertainment, hobby projects, amateur pursuits, or religious
|
||||
observance, without any anticipated commercial application,
|
||||
is use for a permitted purpose.
|
||||
|
||||
## Fair Use
|
||||
|
||||
You may have "fair use" rights for the software under the
|
||||
law. These terms do not limit them.
|
||||
|
||||
## No Other Rights
|
||||
|
||||
These terms do not allow you to sublicense or transfer any of
|
||||
your licenses to anyone else, or prevent the licensor from
|
||||
granting licenses to anyone else. These terms do not imply
|
||||
any other licenses.
|
||||
|
||||
## Patent Defense
|
||||
|
||||
If you make any written claim that the software infringes or
|
||||
contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If
|
||||
your company makes such a claim, your patent license ends
|
||||
immediately for work on behalf of your company.
|
||||
|
||||
## Violations
|
||||
|
||||
The first time you are notified in writing that you have
|
||||
violated any of these terms, or done anything with the software
|
||||
not covered by your licenses, your licenses can nonetheless
|
||||
continue if you come into full compliance with these terms,
|
||||
and take practical steps to correct past violations, within
|
||||
32 days of receiving notice. Otherwise, all your licenses
|
||||
end immediately.
|
||||
|
||||
## No Liability
|
||||
|
||||
***As far as the law allows, the software comes as is, without
|
||||
any warranty or condition, and the licensor will not be liable
|
||||
to you for any damages arising out of these terms or the use
|
||||
or nature of the software, under any kind of legal claim.***
|
||||
|
||||
## Definitions
|
||||
|
||||
The **licensor** is the individual or entity offering these
|
||||
terms, and the **software** is the software the licensor makes
|
||||
available under these terms.
|
||||
|
||||
**You** refers to the individual or entity agreeing to these
|
||||
terms.
|
||||
|
||||
**Your company** is any legal entity, sole proprietorship,
|
||||
or other kind of organization that you work for, plus all
|
||||
organizations that have control over, are under the control of,
|
||||
or are under common control with that organization. **Control**
|
||||
means ownership of substantially all the assets of an entity,
|
||||
or the power to direct its management and policies by vote,
|
||||
contract, or otherwise. Control can be direct or indirect.
|
||||
|
||||
**Your licenses** are all the licenses granted to you for the
|
||||
software under these terms.
|
||||
|
||||
**Use** means anything you do with the software requiring one
|
||||
of your licenses.
|
||||
Generated
+179
@@ -0,0 +1,179 @@
|
||||
{
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1766194365,
|
||||
"narHash": "sha256-4AFsUZ0kl6MXSm4BaQgItD0VGlEKR3iq7gIaL7TjBvc=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "7d8ec2c71771937ab99790b45e6d9b93d15d9379",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1761588595,
|
||||
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1765835352,
|
||||
"narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "a34fae9c08a15ad73f295041fec82323541400a9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks-nix": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1765911976,
|
||||
"narHash": "sha256-t3T/xm8zstHRLx+pIHxVpQTiySbKqcQbK+r+01XVKc0=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "b68b780b69702a090c8bb1b973bab13756cc7a27",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"git-hooks-nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766309749,
|
||||
"narHash": "sha256-3xY8CZ4rSnQ0NqGhMKAy5vgC+2IVK0NoVEzDoOh4DA4=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a6531044f6d0bef691ea18d4d4ce44d0daa6e816",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1765674936,
|
||||
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"crane": "crane",
|
||||
"flake-parts": "flake-parts",
|
||||
"git-hooks-nix": "git-hooks-nix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1766544144,
|
||||
"narHash": "sha256-5ppfEyZqX6FMluZHty7Dvw4xN10vLq1yQosp7dGG/28=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "5359a4fdc3fa9baa2edcf49758d404dfeeca7743",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1766000401,
|
||||
"narHash": "sha256-+cqN4PJz9y0JQXfAK5J1drd0U05D5fcAGhzhfVrDlsI=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
crane.url = "github:ipetkov/crane";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
|
||||
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
git-hooks-nix.url = "github:cachix/git-hooks.nix";
|
||||
git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs@{
|
||||
nixpkgs,
|
||||
flake-parts,
|
||||
crane,
|
||||
rust-overlay,
|
||||
...
|
||||
}:
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
imports = [
|
||||
inputs.treefmt-nix.flakeModule
|
||||
inputs.git-hooks-nix.flakeModule
|
||||
];
|
||||
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
|
||||
perSystem =
|
||||
{
|
||||
config,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [ rust-overlay.overlays.default ];
|
||||
};
|
||||
|
||||
rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
|
||||
nightlyRustfmt = pkgs.rust-bin.nightly.latest.rustfmt;
|
||||
rustToolchainWithNightlyRustfmt = pkgs.symlinkJoin {
|
||||
name = "rust-toolchain-with-nightly-rustfmt";
|
||||
paths = [
|
||||
nightlyRustfmt
|
||||
rustToolchain
|
||||
];
|
||||
};
|
||||
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||
|
||||
src = pkgs.lib.cleanSourceWith {
|
||||
src = ./.;
|
||||
filter = craneLib.filterCargoSources;
|
||||
};
|
||||
|
||||
commonArgs = {
|
||||
inherit src;
|
||||
strictDeps = true;
|
||||
};
|
||||
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
schemars-nixgen = craneLib.buildPackage (
|
||||
commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
checks = {
|
||||
clippy = craneLib.cargoClippy (
|
||||
commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
}
|
||||
);
|
||||
tests = craneLib.cargoTest (
|
||||
commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
packages = {
|
||||
inherit schemars-nixgen;
|
||||
default = schemars-nixgen;
|
||||
};
|
||||
|
||||
devShells.default = pkgs.mkShell {
|
||||
inputsFrom = [ schemars-nixgen ];
|
||||
packages = [
|
||||
rustToolchainWithNightlyRustfmt
|
||||
pkgs.bacon
|
||||
];
|
||||
};
|
||||
|
||||
pre-commit = {
|
||||
check.enable = true;
|
||||
settings.hooks.treefmt = {
|
||||
enable = true;
|
||||
package = config.treefmt.build.wrapper;
|
||||
pass_filenames = false;
|
||||
};
|
||||
};
|
||||
|
||||
treefmt = {
|
||||
projectRootFile = "flake.nix";
|
||||
programs = {
|
||||
rustfmt = {
|
||||
enable = true;
|
||||
package = nightlyRustfmt;
|
||||
};
|
||||
taplo.enable = true;
|
||||
nixfmt.enable = true;
|
||||
deadnix.enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
//! Nix code formatting utilities
|
||||
|
||||
/// Generate the Nix module header
|
||||
pub fn module_header() -> &'static str {
|
||||
"{ lib, ... }:"
|
||||
}
|
||||
|
||||
/// Generate the opening of an options block
|
||||
pub fn options_block_open(option_name: &str) -> String {
|
||||
format!(
|
||||
r#"{{
|
||||
options.{} = lib.mkOption {{
|
||||
type = lib.types.submodule {{
|
||||
options = {{"#,
|
||||
option_name
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate the closing of an options block
|
||||
pub fn options_block_close(description: &str) -> String {
|
||||
format!(
|
||||
r#" }};
|
||||
}};
|
||||
default = {{}};
|
||||
description = "{}";
|
||||
}};
|
||||
}}"#,
|
||||
crate::types::escape_nix_string(description)
|
||||
)
|
||||
}
|
||||
|
||||
/// Indent a block of text
|
||||
pub fn indent(text: &str, level: usize) -> String {
|
||||
let prefix = " ".repeat(level);
|
||||
text.lines()
|
||||
.map(|line| {
|
||||
if line.trim().is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("{}{}", prefix, line)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// Convert a JSON value to a Nix literal
|
||||
pub fn json_to_nix_literal(value: &serde_json::Value) -> String {
|
||||
match value {
|
||||
serde_json::Value::Null => "null".to_string(),
|
||||
serde_json::Value::Bool(b) => b.to_string(),
|
||||
serde_json::Value::Number(n) => {
|
||||
if let Some(i) = n.as_i64() {
|
||||
i.to_string()
|
||||
} else if let Some(f) = n.as_f64() {
|
||||
format!("{:.6}", f)
|
||||
.trim_end_matches('0')
|
||||
.trim_end_matches('.')
|
||||
.to_string()
|
||||
} else {
|
||||
n.to_string()
|
||||
}
|
||||
}
|
||||
serde_json::Value::String(s) => {
|
||||
format!("\"{}\"", crate::types::escape_nix_string(s))
|
||||
}
|
||||
serde_json::Value::Array(arr) => {
|
||||
let items: Vec<String> = arr.iter().map(json_to_nix_literal).collect();
|
||||
format!("[ {} ]", items.join(" "))
|
||||
}
|
||||
serde_json::Value::Object(obj) => {
|
||||
if obj.is_empty() {
|
||||
"{}".to_string()
|
||||
} else {
|
||||
let attrs: Vec<String> = obj
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
format!(
|
||||
"{} = {};",
|
||||
crate::types::escape_nix_attr_name(k),
|
||||
json_to_nix_literal(v)
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
format!("{{ {} }}", attrs.join(" "))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_json_to_nix_literal_primitives() {
|
||||
assert_eq!(json_to_nix_literal(&json!(null)), "null");
|
||||
assert_eq!(json_to_nix_literal(&json!(true)), "true");
|
||||
assert_eq!(json_to_nix_literal(&json!(false)), "false");
|
||||
assert_eq!(json_to_nix_literal(&json!(42)), "42");
|
||||
assert_eq!(json_to_nix_literal(&json!(2.5)), "2.5");
|
||||
assert_eq!(json_to_nix_literal(&json!("hello")), "\"hello\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_to_nix_literal_array() {
|
||||
assert_eq!(json_to_nix_literal(&json!([1, 2, 3])), "[ 1 2 3 ]");
|
||||
assert_eq!(json_to_nix_literal(&json!(["a", "b"])), "[ \"a\" \"b\" ]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_json_to_nix_literal_object() {
|
||||
assert_eq!(json_to_nix_literal(&json!({})), "{}");
|
||||
// Note: Object ordering may vary, so we just check it produces valid output
|
||||
let result = json_to_nix_literal(&json!({"x": 1}));
|
||||
assert!(result.contains("x = 1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_indent() {
|
||||
let text = "line1\nline2\nline3";
|
||||
let indented = indent(text, 2);
|
||||
assert_eq!(indented, " line1\n line2\n line3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_module_header() {
|
||||
assert_eq!(module_header(), "{ lib, ... }:");
|
||||
}
|
||||
}
|
||||
+1555
File diff suppressed because it is too large.
Load diff
+103
@@ -0,0 +1,103 @@
|
||||
//! schemars-nixgen: Generate Nix option definitions from JSON Schema
|
||||
//!
|
||||
//! This library converts JSON Schema (as generated by schemars) into
|
||||
//! strongly-typed Nix option definitions for the NixOS module system.
|
||||
|
||||
pub mod format;
|
||||
pub mod generator;
|
||||
pub mod prepare;
|
||||
pub mod types;
|
||||
|
||||
pub use generator::Generator;
|
||||
pub use generator::GeneratorError;
|
||||
pub use generator::GeneratorOptions;
|
||||
pub use types::NixOption;
|
||||
pub use types::NixType;
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde_json::Value;
|
||||
|
||||
/// Extension trait for generating Nix options from types that implement JsonSchema
|
||||
pub trait SchemaExt: JsonSchema {
|
||||
/// Generate Nix module code for this type
|
||||
fn to_nix_options(options: &GeneratorOptions) -> Result<String, GeneratorError> {
|
||||
let schema = schemars::schema_for!(Self);
|
||||
let schema_value: Value = serde_json::to_value(&schema)
|
||||
.map_err(|e| GeneratorError::InvalidSchema(e.to_string()))?;
|
||||
let mut generator = Generator::new(&schema_value);
|
||||
generator.generate(&schema_value, options)
|
||||
}
|
||||
}
|
||||
|
||||
// Implement SchemaExt for all types that implement JsonSchema
|
||||
impl<T: JsonSchema> SchemaExt for T {}
|
||||
|
||||
/// Generate Nix options from a JSON schema value
|
||||
pub fn generate_from_schema(
|
||||
schema: &Value,
|
||||
options: &GeneratorOptions,
|
||||
) -> Result<String, GeneratorError> {
|
||||
let mut generator = Generator::new(schema);
|
||||
generator.generate(schema, options)
|
||||
}
|
||||
|
||||
/// Generate Nix options from a JSON schema string
|
||||
pub fn generate_from_str(
|
||||
schema_str: &str,
|
||||
options: &GeneratorOptions,
|
||||
) -> Result<String, GeneratorError> {
|
||||
let schema: Value = serde_json::from_str(schema_str)
|
||||
.map_err(|e| GeneratorError::InvalidSchema(e.to_string()))?;
|
||||
generate_from_schema(&schema, options)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_generate_from_schema() {
|
||||
let schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to enable the feature"
|
||||
},
|
||||
"count": {
|
||||
"type": "integer",
|
||||
"description": "Number of items"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let options = GeneratorOptions {
|
||||
root_name: "myapp".to_string(),
|
||||
root_description: "My application configuration".to_string(),
|
||||
};
|
||||
|
||||
let output = generate_from_schema(&schema, &options).unwrap();
|
||||
|
||||
assert!(output.contains("{ lib, ... }:"));
|
||||
assert!(output.contains("options.myapp"));
|
||||
assert!(output.contains("lib.types.bool"));
|
||||
assert!(output.contains("lib.types.int"));
|
||||
assert!(output.contains("Whether to enable the feature"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generate_from_str() {
|
||||
let schema_str = r#"{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let options = GeneratorOptions::default();
|
||||
let output = generate_from_str(schema_str, &options).unwrap();
|
||||
|
||||
assert!(output.contains("lib.types.str"));
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
use clap::Parser;
|
||||
use schemars_nixgen::GeneratorOptions;
|
||||
use schemars_nixgen::generate_from_schema;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::io::{self};
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Generate Nix option definitions from JSON Schema
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "schemars-nixgen")]
|
||||
#[command(about = "Generate Nix option definitions from JSON Schema")]
|
||||
struct Cli {
|
||||
/// Input JSON Schema file
|
||||
input: PathBuf,
|
||||
|
||||
/// Output Nix file (default: stdout)
|
||||
#[arg(short, long)]
|
||||
output: Option<PathBuf>,
|
||||
|
||||
/// Root option name (e.g., "komorebi" or "komorebi-bar")
|
||||
#[arg(short, long, default_value = "config")]
|
||||
name: String,
|
||||
|
||||
/// Description for the root option
|
||||
#[arg(short, long, default_value = "Configuration options")]
|
||||
description: String,
|
||||
}
|
||||
|
||||
fn main() -> color_eyre::Result<()> {
|
||||
color_eyre::install()?;
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Read the input schema
|
||||
let schema_str = fs::read_to_string(&cli.input)?;
|
||||
let schema: serde_json::Value = serde_json::from_str(&schema_str)?;
|
||||
|
||||
// Configure generator options
|
||||
let options = GeneratorOptions {
|
||||
root_name: cli.name,
|
||||
root_description: cli.description,
|
||||
};
|
||||
|
||||
// Generate Nix code
|
||||
let nix_code = generate_from_schema(&schema, &options)?;
|
||||
|
||||
// Write output
|
||||
if let Some(output_path) = cli.output {
|
||||
fs::write(&output_path, &nix_code)?;
|
||||
eprintln!(
|
||||
"Generated Nix options written to: {}",
|
||||
output_path.display()
|
||||
);
|
||||
} else {
|
||||
io::stdout().write_all(nix_code.as_bytes())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+503
@@ -0,0 +1,503 @@
|
||||
//! Schema preparation utilities for normalizing JSON Schema before conversion
|
||||
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Result type for mixed enum + object union detection
|
||||
/// Contains (enum_variants, object_variants) where object_variants are (tag_name, inner_schema) tuples
|
||||
pub type MixedUnionResult = (Vec<String>, Vec<(String, Value)>);
|
||||
|
||||
/// Extract all definitions from the schema's $defs section
|
||||
pub fn collect_definitions(schema: &Value) -> HashMap<String, Value> {
|
||||
let mut defs = HashMap::new();
|
||||
|
||||
if let Some(definitions) = schema.get("$defs").and_then(|d| d.as_object()) {
|
||||
for (name, def) in definitions {
|
||||
defs.insert(name.clone(), def.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Also check "definitions" for older schemas
|
||||
if let Some(definitions) = schema.get("definitions").and_then(|d| d.as_object()) {
|
||||
for (name, def) in definitions {
|
||||
defs.insert(name.clone(), def.clone());
|
||||
}
|
||||
}
|
||||
|
||||
defs
|
||||
}
|
||||
|
||||
/// Resolve a $ref to its definition name
|
||||
pub fn resolve_ref_name(ref_str: &str) -> Option<&str> {
|
||||
// Handle "#/$defs/TypeName" or "#/definitions/TypeName"
|
||||
if let Some(name) = ref_str.strip_prefix("#/$defs/") {
|
||||
Some(name)
|
||||
} else if let Some(name) = ref_str.strip_prefix("#/definitions/") {
|
||||
Some(name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a schema represents a nullable type (anyOf with null)
|
||||
/// Returns Some(inner_schema) if it's a nullable pattern, None otherwise
|
||||
/// Handles cases with multiple null variants (e.g., from serde Option serialization)
|
||||
pub fn detect_nullable_anyof(schema: &Value) -> Option<&Value> {
|
||||
let any_of = schema.get("anyOf")?.as_array()?;
|
||||
|
||||
// Collect null and non-null variants
|
||||
let mut non_null_variants: Vec<&Value> = Vec::new();
|
||||
let mut has_null = false;
|
||||
|
||||
for variant in any_of.iter() {
|
||||
if is_null_schema(variant) {
|
||||
has_null = true;
|
||||
} else {
|
||||
non_null_variants.push(variant);
|
||||
}
|
||||
}
|
||||
|
||||
// If we have exactly one non-null variant and at least one null, it's a nullable pattern
|
||||
if has_null && non_null_variants.len() == 1 {
|
||||
Some(non_null_variants[0])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a schema is a null type
|
||||
pub fn is_null_schema(schema: &Value) -> bool {
|
||||
// Check for {"type": "null"}
|
||||
schema
|
||||
.get("type")
|
||||
.is_some_and(|type_val| type_val == "null")
|
||||
}
|
||||
|
||||
/// Check if a type array includes null: ["string", "null"]
|
||||
/// Returns Some(non_null_type) if it's a nullable type array
|
||||
pub fn detect_nullable_type_array(schema: &Value) -> Option<String> {
|
||||
let type_val = schema.get("type")?;
|
||||
let type_array = type_val.as_array()?;
|
||||
|
||||
if type_array.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut has_null = false;
|
||||
let mut other_type = None;
|
||||
|
||||
for t in type_array {
|
||||
if let Some(t_str) = t.as_str() {
|
||||
if t_str == "null" {
|
||||
has_null = true;
|
||||
} else {
|
||||
other_type = Some(t_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if has_null { other_type } else { None }
|
||||
}
|
||||
|
||||
/// Detect enum pattern from oneOf with const variants
|
||||
/// Returns Some(variants) if all variants are const strings
|
||||
pub fn detect_const_enum(schema: &Value) -> Option<Vec<String>> {
|
||||
let one_of = schema.get("oneOf")?.as_array()?;
|
||||
|
||||
let mut variants = Vec::new();
|
||||
|
||||
for variant in one_of {
|
||||
// Check for {"const": "value"}
|
||||
if let Some(const_val) = variant.get("const")
|
||||
&& let Some(s) = const_val.as_str()
|
||||
{
|
||||
variants.push(s.to_string());
|
||||
continue;
|
||||
}
|
||||
// If any variant is not a const string, this isn't a simple enum
|
||||
return None;
|
||||
}
|
||||
|
||||
if variants.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(variants)
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect simple enum from "enum" array
|
||||
pub fn detect_simple_enum(schema: &Value) -> Option<Vec<String>> {
|
||||
let enum_val = schema.get("enum")?.as_array()?;
|
||||
|
||||
let variants: Vec<String> = enum_val
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str().map(String::from))
|
||||
.collect();
|
||||
|
||||
if variants.len() == enum_val.len() && !variants.is_empty() {
|
||||
Some(variants)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the primary type from a schema
|
||||
pub fn get_schema_type(schema: &Value) -> Option<&str> {
|
||||
schema.get("type")?.as_str()
|
||||
}
|
||||
|
||||
/// Get the description from a schema
|
||||
pub fn get_description(schema: &Value) -> Option<&str> {
|
||||
schema.get("description")?.as_str()
|
||||
}
|
||||
|
||||
/// Get the default value from a schema
|
||||
pub fn get_default(schema: &Value) -> Option<&Value> {
|
||||
schema.get("default")
|
||||
}
|
||||
|
||||
/// Check if schema has a $ref
|
||||
pub fn get_ref(schema: &Value) -> Option<&str> {
|
||||
schema.get("$ref")?.as_str()
|
||||
}
|
||||
|
||||
/// Get properties from an object schema
|
||||
pub fn get_properties(schema: &Value) -> Option<&serde_json::Map<String, Value>> {
|
||||
schema.get("properties")?.as_object()
|
||||
}
|
||||
|
||||
/// Get the items schema for an array
|
||||
pub fn get_array_items(schema: &Value) -> Option<&Value> {
|
||||
schema.get("items")
|
||||
}
|
||||
|
||||
/// Get the prefixItems for a tuple array schema
|
||||
/// Returns the first item type (for homogeneous tuples like [int, int])
|
||||
pub fn get_prefix_items_type(schema: &Value) -> Option<&Value> {
|
||||
let prefix_items = schema.get("prefixItems")?.as_array()?;
|
||||
// Return the first item's schema - for homogeneous tuples this represents all items
|
||||
prefix_items.first()
|
||||
}
|
||||
|
||||
/// Get additionalProperties schema
|
||||
pub fn get_additional_properties(schema: &Value) -> Option<&Value> {
|
||||
let ap = schema.get("additionalProperties")?;
|
||||
// additionalProperties can be a boolean or a schema
|
||||
if ap.is_boolean() { None } else { Some(ap) }
|
||||
}
|
||||
|
||||
/// Get patternProperties schemas (returns first one for simplicity)
|
||||
pub fn get_pattern_properties(schema: &Value) -> Option<&Value> {
|
||||
let pp = schema.get("patternProperties")?.as_object()?;
|
||||
// Return the first pattern's schema
|
||||
pp.values().next()
|
||||
}
|
||||
|
||||
/// Detect externally tagged union pattern (oneOf with single-property object variants)
|
||||
/// This is the Serde default representation for Rust enums with data.
|
||||
/// Example: `oneOf: [{ "Komorebi": {...} }, { "Battery": {...} }]`
|
||||
/// Returns Some(vec of (tag_name, inner_schema)) if detected
|
||||
///
|
||||
/// IMPORTANT: Each variant must have a DIFFERENT tag name. If all variants have the same
|
||||
/// property name (like "kind"), this is an internally-tagged union, not externally-tagged.
|
||||
pub fn detect_externally_tagged_union(schema: &Value) -> Option<Vec<(String, Value)>> {
|
||||
let one_of = schema.get("oneOf")?.as_array()?;
|
||||
|
||||
// Need at least 2 variants for a tagged union
|
||||
if one_of.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut variants = Vec::new();
|
||||
|
||||
for variant in one_of {
|
||||
// Check if this variant is an object with exactly one required property
|
||||
if let Some(props) = variant.get("properties").and_then(|p| p.as_object())
|
||||
&& let Some(required) = variant.get("required").and_then(|r| r.as_array())
|
||||
{
|
||||
// Must have exactly one property and one required field
|
||||
if required.len() == 1
|
||||
&& props.len() == 1
|
||||
&& let Some(tag) = required[0].as_str()
|
||||
&& let Some(inner) = props.get(tag)
|
||||
{
|
||||
variants.push((tag.to_string(), inner.clone()));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// If any variant doesn't match the pattern, this isn't an externally tagged union
|
||||
return None;
|
||||
}
|
||||
|
||||
if variants.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Check that all tag names are UNIQUE (externally tagged unions have different keys)
|
||||
// If all variants have the same key, this is an internally-tagged union
|
||||
let first_tag = &variants[0].0;
|
||||
let all_same = variants.iter().all(|(tag, _)| tag == first_tag);
|
||||
if all_same {
|
||||
// All variants have the same property name - this is internally tagged, not externally
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(variants)
|
||||
}
|
||||
|
||||
/// Detect internally tagged union pattern (oneOf with shared discriminator field containing const)
|
||||
/// Example: `oneOf: [{ "kind": "None" }, { "kind": "Bar", ...config }]`
|
||||
/// Returns Some((discriminator_field, vec of (discriminator_value, variant_schema)))
|
||||
pub fn detect_internally_tagged_union(schema: &Value) -> Option<(String, Vec<(String, Value)>)> {
|
||||
let one_of = schema.get("oneOf")?.as_array()?;
|
||||
|
||||
if one_of.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find common discriminator field with const values
|
||||
let mut discriminator_field: Option<String> = None;
|
||||
let mut variants = Vec::new();
|
||||
|
||||
for variant in one_of {
|
||||
let props = variant.get("properties")?.as_object()?;
|
||||
|
||||
// Look for a property with a const value (the discriminator)
|
||||
let mut found_discriminator = false;
|
||||
for (key, prop_schema) in props {
|
||||
if let Some(const_val) = prop_schema.get("const").and_then(|c| c.as_str()) {
|
||||
if discriminator_field.is_none() {
|
||||
discriminator_field = Some(key.clone());
|
||||
}
|
||||
if discriminator_field.as_ref() == Some(key) {
|
||||
variants.push((const_val.to_string(), variant.clone()));
|
||||
found_discriminator = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found_discriminator {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let disc = discriminator_field?;
|
||||
if variants.len() == one_of.len() {
|
||||
Some((disc, variants))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect mixed enum + object union (oneOf with string consts AND tagged object variants)
|
||||
/// Example: DateFormat with "MonthDateYear" (string) OR { "Custom": "..." } (object)
|
||||
/// Returns Some((enum_variants, object_variants)) if detected
|
||||
pub fn detect_mixed_enum_object_union(schema: &Value) -> Option<MixedUnionResult> {
|
||||
let one_of = schema.get("oneOf")?.as_array()?;
|
||||
|
||||
let mut enum_variants = Vec::new();
|
||||
let mut object_variants = Vec::new();
|
||||
|
||||
for variant in one_of {
|
||||
// Check for string const
|
||||
if variant.get("type").and_then(|t| t.as_str()) == Some("string") {
|
||||
if let Some(const_val) = variant.get("const").and_then(|c| c.as_str()) {
|
||||
enum_variants.push(const_val.to_string());
|
||||
continue;
|
||||
}
|
||||
// Also handle enum arrays in string type
|
||||
if let Some(enum_vals) = variant.get("enum").and_then(|e| e.as_array()) {
|
||||
for v in enum_vals {
|
||||
if let Some(s) = v.as_str() {
|
||||
enum_variants.push(s.to_string());
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for object with single required property (externally tagged variant)
|
||||
if let Some(props) = variant.get("properties").and_then(|p| p.as_object())
|
||||
&& let Some(required) = variant.get("required").and_then(|r| r.as_array())
|
||||
&& required.len() == 1
|
||||
&& props.len() == 1
|
||||
&& let Some(tag) = required[0].as_str()
|
||||
&& let Some(inner) = props.get(tag)
|
||||
{
|
||||
object_variants.push((tag.to_string(), inner.clone()));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Variant doesn't match either pattern - could still be valid if we have some of each
|
||||
}
|
||||
|
||||
// Only return if we have BOTH enum variants AND object variants
|
||||
if !enum_variants.is_empty() && !object_variants.is_empty() {
|
||||
Some((enum_variants, object_variants))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_ref_name() {
|
||||
assert_eq!(resolve_ref_name("#/$defs/MyType"), Some("MyType"));
|
||||
assert_eq!(resolve_ref_name("#/definitions/OldType"), Some("OldType"));
|
||||
assert_eq!(resolve_ref_name("invalid"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_nullable_anyof() {
|
||||
let schema = json!({
|
||||
"anyOf": [
|
||||
{"$ref": "#/$defs/Color"},
|
||||
{"type": "null"}
|
||||
]
|
||||
});
|
||||
let inner = detect_nullable_anyof(&schema).unwrap();
|
||||
assert_eq!(inner.get("$ref").unwrap(), "#/$defs/Color");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_nullable_type_array() {
|
||||
let schema = json!({
|
||||
"type": ["string", "null"]
|
||||
});
|
||||
assert_eq!(
|
||||
detect_nullable_type_array(&schema),
|
||||
Some("string".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_const_enum() {
|
||||
let schema = json!({
|
||||
"oneOf": [
|
||||
{"const": "Left"},
|
||||
{"const": "Right"},
|
||||
{"const": "Center"}
|
||||
]
|
||||
});
|
||||
let variants = detect_const_enum(&schema).unwrap();
|
||||
assert_eq!(variants, vec!["Left", "Right", "Center"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_simple_enum() {
|
||||
let schema = json!({
|
||||
"enum": ["Red", "Green", "Blue"]
|
||||
});
|
||||
let variants = detect_simple_enum(&schema).unwrap();
|
||||
assert_eq!(variants, vec!["Red", "Green", "Blue"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_collect_definitions() {
|
||||
let schema = json!({
|
||||
"$defs": {
|
||||
"Color": {"type": "string"},
|
||||
"Size": {"type": "integer"}
|
||||
}
|
||||
});
|
||||
let defs = collect_definitions(&schema);
|
||||
assert_eq!(defs.len(), 2);
|
||||
assert!(defs.contains_key("Color"));
|
||||
assert!(defs.contains_key("Size"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_externally_tagged_union() {
|
||||
// Pattern like WidgetConfig: { "Komorebi": {...} } | { "Battery": {...} }
|
||||
let schema = json!({
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Komorebi": {"$ref": "#/$defs/KomorebiConfig"}
|
||||
},
|
||||
"required": ["Komorebi"]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Battery": {"$ref": "#/$defs/BatteryConfig"}
|
||||
},
|
||||
"required": ["Battery"]
|
||||
}
|
||||
]
|
||||
});
|
||||
let variants = detect_externally_tagged_union(&schema).unwrap();
|
||||
assert_eq!(variants.len(), 2);
|
||||
assert_eq!(variants[0].0, "Komorebi");
|
||||
assert_eq!(variants[1].0, "Battery");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_externally_tagged_union_not_matching() {
|
||||
// Not a tagged union - just const enums
|
||||
let schema = json!({
|
||||
"oneOf": [
|
||||
{"type": "string", "const": "Left"},
|
||||
{"type": "string", "const": "Right"}
|
||||
]
|
||||
});
|
||||
assert!(detect_externally_tagged_union(&schema).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_internally_tagged_union() {
|
||||
// Pattern like Grouping: { "kind": "None" } | { "kind": "Bar", ...config }
|
||||
let schema = json!({
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {"type": "string", "const": "None"}
|
||||
},
|
||||
"required": ["kind"]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {"type": "string", "const": "Bar"},
|
||||
"transparency": {"type": "number"}
|
||||
},
|
||||
"required": ["kind"]
|
||||
}
|
||||
]
|
||||
});
|
||||
let (disc, variants) = detect_internally_tagged_union(&schema).unwrap();
|
||||
assert_eq!(disc, "kind");
|
||||
assert_eq!(variants.len(), 2);
|
||||
assert_eq!(variants[0].0, "None");
|
||||
assert_eq!(variants[1].0, "Bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_mixed_enum_object_union() {
|
||||
// Pattern like DateFormat: "MonthDateYear" | { "Custom": "..." }
|
||||
let schema = json!({
|
||||
"oneOf": [
|
||||
{"type": "string", "const": "MonthDateYear"},
|
||||
{"type": "string", "const": "YearMonthDate"},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"Custom": {"type": "string"}
|
||||
},
|
||||
"required": ["Custom"]
|
||||
}
|
||||
]
|
||||
});
|
||||
let (enums, objects) = detect_mixed_enum_object_union(&schema).unwrap();
|
||||
assert_eq!(enums, vec!["MonthDateYear", "YearMonthDate"]);
|
||||
assert_eq!(objects.len(), 1);
|
||||
assert_eq!(objects[0].0, "Custom");
|
||||
}
|
||||
}
|
||||
+332
@@ -0,0 +1,332 @@
|
||||
/// Represents a Nix type expression
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum NixType {
|
||||
/// lib.types.str
|
||||
Str,
|
||||
/// lib.types.int
|
||||
Int,
|
||||
/// lib.types.bool
|
||||
Bool,
|
||||
/// lib.types.float (actually lib.types.number in Nix)
|
||||
Float,
|
||||
/// lib.types.path
|
||||
Path,
|
||||
/// lib.types.nullOr <inner>
|
||||
NullOr(Box<NixType>),
|
||||
/// lib.types.listOf <inner>
|
||||
ListOf(Box<NixType>),
|
||||
/// lib.types.attrsOf <inner>
|
||||
AttrsOf(Box<NixType>),
|
||||
/// lib.types.enum [ "A" "B" ... ]
|
||||
Enum(Vec<String>),
|
||||
/// lib.types.oneOf [ ... ]
|
||||
OneOf(Vec<NixType>),
|
||||
/// lib.types.submodule { options = { ... }; }
|
||||
Submodule(Vec<NixOption>),
|
||||
/// Tagged union: submodule where each variant is an optional field
|
||||
/// Generated from externally tagged unions like WidgetConfig
|
||||
/// Each tuple is (tag_name, inner_type)
|
||||
TaggedUnion(Vec<(String, NixType)>),
|
||||
/// Reference to a named type defined in a let binding
|
||||
TypeRef(String),
|
||||
/// lib.types.anything - fallback for complex types
|
||||
Anything,
|
||||
/// Raw Nix expression for edge cases
|
||||
Raw(String),
|
||||
}
|
||||
|
||||
/// Represents a Nix option definition
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct NixOption {
|
||||
pub name: String,
|
||||
pub nix_type: NixType,
|
||||
pub description: Option<String>,
|
||||
pub default: Option<String>,
|
||||
pub example: Option<String>,
|
||||
}
|
||||
|
||||
impl NixType {
|
||||
/// Convert this type to a Nix expression string
|
||||
pub fn to_nix_expr(&self) -> String {
|
||||
match self {
|
||||
NixType::Str => "lib.types.str".to_string(),
|
||||
NixType::Int => "lib.types.int".to_string(),
|
||||
NixType::Bool => "lib.types.bool".to_string(),
|
||||
NixType::Float => "lib.types.number".to_string(),
|
||||
NixType::Path => "lib.types.path".to_string(),
|
||||
NixType::NullOr(inner) => {
|
||||
format!("(lib.types.nullOr {})", inner.to_nix_expr())
|
||||
}
|
||||
NixType::ListOf(inner) => {
|
||||
format!("(lib.types.listOf {})", inner.to_nix_expr())
|
||||
}
|
||||
NixType::AttrsOf(inner) => {
|
||||
format!("(lib.types.attrsOf {})", inner.to_nix_expr())
|
||||
}
|
||||
NixType::Enum(variants) => {
|
||||
let variants_str: Vec<String> = variants
|
||||
.iter()
|
||||
.map(|v| format!("\"{}\"", escape_nix_string(v)))
|
||||
.collect();
|
||||
format!("(lib.types.enum [ {} ])", variants_str.join(" "))
|
||||
}
|
||||
NixType::OneOf(types) => {
|
||||
let types_str: Vec<String> = types.iter().map(|t| t.to_nix_expr()).collect();
|
||||
format!("(lib.types.oneOf [ {} ])", types_str.join(" "))
|
||||
}
|
||||
NixType::Submodule(options) => {
|
||||
if options.is_empty() {
|
||||
"(lib.types.attrsOf lib.types.anything)".to_string()
|
||||
} else {
|
||||
let opts: Vec<String> =
|
||||
options.iter().map(|opt| opt.to_nix_option_def(2)).collect();
|
||||
format!(
|
||||
"(lib.types.submodule {{\n options = {{\n{}\n }};\n }})",
|
||||
opts.join("\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
NixType::TaggedUnion(variants) => {
|
||||
// Generate submodule with each variant as nullOr option with default = null
|
||||
// This transforms externally-tagged unions into a single submodule where
|
||||
// users specify exactly one variant (the others remain null)
|
||||
if variants.is_empty() {
|
||||
"(lib.types.attrsOf lib.types.anything)".to_string()
|
||||
} else {
|
||||
let opts: Vec<String> = variants
|
||||
.iter()
|
||||
.map(|(tag, inner_type)| {
|
||||
// Each variant becomes: Tag = lib.mkOption { type = lib.types.nullOr <inner>; default = null; };
|
||||
let inner_expr = inner_type.to_nix_expr();
|
||||
let tag_escaped = escape_nix_attr_name(tag);
|
||||
format!(
|
||||
" {} = lib.mkOption {{ type = (lib.types.nullOr {}); default = null; }};",
|
||||
tag_escaped,
|
||||
inner_expr
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
format!(
|
||||
"(lib.types.submodule {{\n options = {{\n{}\n }};\n }})",
|
||||
opts.join("\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
NixType::TypeRef(name) => name.clone(),
|
||||
NixType::Anything => "lib.types.anything".to_string(),
|
||||
NixType::Raw(expr) => expr.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this type is nullable (NullOr)
|
||||
pub fn is_nullable(&self) -> bool {
|
||||
matches!(self, NixType::NullOr(_))
|
||||
}
|
||||
|
||||
/// Wrap this type in NullOr if not already nullable
|
||||
pub fn make_nullable(self) -> NixType {
|
||||
if self.is_nullable() {
|
||||
self
|
||||
} else {
|
||||
NixType::NullOr(Box::new(self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NixOption {
|
||||
/// Create a new NixOption
|
||||
pub fn new(name: impl Into<String>, nix_type: NixType) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
nix_type,
|
||||
description: None,
|
||||
default: None,
|
||||
example: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the description
|
||||
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
|
||||
self.description = Some(desc.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the default value
|
||||
pub fn with_default(mut self, default: impl Into<String>) -> Self {
|
||||
self.default = Some(default.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Generate the Nix option definition code
|
||||
pub fn to_nix_option_def(&self, indent_level: usize) -> String {
|
||||
let indent = " ".repeat(indent_level);
|
||||
let inner_indent = " ".repeat(indent_level + 1);
|
||||
|
||||
let mut parts = vec![format!(
|
||||
"{}type = {};",
|
||||
inner_indent,
|
||||
self.nix_type.to_nix_expr()
|
||||
)];
|
||||
|
||||
// Add default if present, otherwise use null for nullable types
|
||||
if let Some(ref default) = self.default {
|
||||
parts.push(format!("{}default = {};", inner_indent, default));
|
||||
} else if self.nix_type.is_nullable() {
|
||||
parts.push(format!("{}default = null;", inner_indent));
|
||||
}
|
||||
|
||||
// Add description if present
|
||||
if let Some(ref desc) = self.description {
|
||||
parts.push(format!(
|
||||
"{}description = \"{}\";",
|
||||
inner_indent,
|
||||
escape_nix_string(desc)
|
||||
));
|
||||
}
|
||||
|
||||
format!(
|
||||
"{}{} = lib.mkOption {{\n{}\n{}}};",
|
||||
indent,
|
||||
escape_nix_attr_name(&self.name),
|
||||
parts.join("\n"),
|
||||
indent
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Escape a string for use in Nix string literals
|
||||
pub fn escape_nix_string(s: &str) -> String {
|
||||
s.replace('\\', "\\\\")
|
||||
.replace('"', "\\\"")
|
||||
.replace('\n', "\\n")
|
||||
.replace('\t', "\\t")
|
||||
.replace("${", "\\${")
|
||||
}
|
||||
|
||||
/// Escape an attribute name if it contains special characters
|
||||
pub fn escape_nix_attr_name(name: &str) -> String {
|
||||
// Nix attribute names that contain hyphens or start with numbers need quoting
|
||||
if name.chars().all(|c| c.is_alphanumeric() || c == '_')
|
||||
&& name.chars().next().is_some_and(|c| !c.is_ascii_digit())
|
||||
{
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("\"{}\"", escape_nix_string(name))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_primitive_types() {
|
||||
assert_eq!(NixType::Str.to_nix_expr(), "lib.types.str");
|
||||
assert_eq!(NixType::Int.to_nix_expr(), "lib.types.int");
|
||||
assert_eq!(NixType::Bool.to_nix_expr(), "lib.types.bool");
|
||||
assert_eq!(NixType::Float.to_nix_expr(), "lib.types.number");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nullable_type() {
|
||||
let nullable_str = NixType::NullOr(Box::new(NixType::Str));
|
||||
assert_eq!(
|
||||
nullable_str.to_nix_expr(),
|
||||
"(lib.types.nullOr lib.types.str)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_type() {
|
||||
let list_of_int = NixType::ListOf(Box::new(NixType::Int));
|
||||
assert_eq!(
|
||||
list_of_int.to_nix_expr(),
|
||||
"(lib.types.listOf lib.types.int)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attrs_type() {
|
||||
let attrs_of_str = NixType::AttrsOf(Box::new(NixType::Str));
|
||||
assert_eq!(
|
||||
attrs_of_str.to_nix_expr(),
|
||||
"(lib.types.attrsOf lib.types.str)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enum_type() {
|
||||
let enum_type = NixType::Enum(vec!["Left".to_string(), "Right".to_string()]);
|
||||
assert_eq!(
|
||||
enum_type.to_nix_expr(),
|
||||
"(lib.types.enum [ \"Left\" \"Right\" ])"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_nix_string() {
|
||||
assert_eq!(escape_nix_string("hello"), "hello");
|
||||
assert_eq!(escape_nix_string("line1\nline2"), "line1\\nline2");
|
||||
assert_eq!(escape_nix_string("say \"hi\""), "say \\\"hi\\\"");
|
||||
assert_eq!(escape_nix_string("${var}"), "\\${var}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_attr_name() {
|
||||
assert_eq!(escape_nix_attr_name("foo"), "foo");
|
||||
assert_eq!(escape_nix_attr_name("foo_bar"), "foo_bar");
|
||||
assert_eq!(escape_nix_attr_name("foo-bar"), "\"foo-bar\"");
|
||||
assert_eq!(escape_nix_attr_name("123start"), "\"123start\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_nullable() {
|
||||
let str_type = NixType::Str;
|
||||
let nullable = str_type.make_nullable();
|
||||
assert!(nullable.is_nullable());
|
||||
assert_eq!(nullable.to_nix_expr(), "(lib.types.nullOr lib.types.str)");
|
||||
|
||||
// Already nullable should not double-wrap
|
||||
let already_nullable = NixType::NullOr(Box::new(NixType::Int));
|
||||
let still_nullable = already_nullable.clone().make_nullable();
|
||||
assert_eq!(still_nullable, already_nullable);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tagged_union_type() {
|
||||
let tagged = NixType::TaggedUnion(vec![
|
||||
("Komorebi".to_string(), NixType::Str),
|
||||
("Battery".to_string(), NixType::Int),
|
||||
]);
|
||||
let expr = tagged.to_nix_expr();
|
||||
|
||||
// Should produce a submodule with optional fields
|
||||
assert!(expr.contains("lib.types.submodule"));
|
||||
assert!(expr.contains("Komorebi = lib.mkOption"));
|
||||
assert!(expr.contains("Battery = lib.mkOption"));
|
||||
assert!(expr.contains("lib.types.nullOr lib.types.str"));
|
||||
assert!(expr.contains("lib.types.nullOr lib.types.int"));
|
||||
assert!(expr.contains("default = null"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tagged_union_empty() {
|
||||
let tagged = NixType::TaggedUnion(vec![]);
|
||||
assert_eq!(
|
||||
tagged.to_nix_expr(),
|
||||
"(lib.types.attrsOf lib.types.anything)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tagged_union_with_submodule() {
|
||||
// Test that tagged union can contain submodule types
|
||||
let inner_submodule = NixType::Submodule(vec![NixOption::new("enable", NixType::Bool)]);
|
||||
let tagged = NixType::TaggedUnion(vec![("Widget".to_string(), inner_submodule)]);
|
||||
let expr = tagged.to_nix_expr();
|
||||
|
||||
assert!(expr.contains("Widget = lib.mkOption"));
|
||||
assert!(expr.contains("lib.types.nullOr"));
|
||||
assert!(expr.contains("lib.types.submodule"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user