Compare commits
No commits in common. "5b0dbd30fd8a442b0f29caee9b8b7eeb5eb485cf" and "070e338c5ce076482561eade8572b6211a2926d3" have entirely different histories.
5b0dbd30fd
...
070e338c5c
39 changed files with 230 additions and 3523 deletions
475
Cargo.lock
generated
475
Cargo.lock
generated
|
|
@ -146,15 +146,6 @@ dependencies = [
|
|||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloca"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
|
|
@ -500,15 +491,6 @@ dependencies = [
|
|||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-polyfill"
|
||||
version = "1.0.3"
|
||||
|
|
@ -951,6 +933,12 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
|
|
@ -1269,9 +1257,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "compact_str"
|
||||
version = "0.9.0"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
|
||||
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
|
||||
dependencies = [
|
||||
"castaway",
|
||||
"cfg-if",
|
||||
|
|
@ -1352,7 +1340,7 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "980c2afde4af43d6a05c5be738f9eae595cff86dce1f38f88b95058a98c027f3"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"crossterm 0.29.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1391,24 +1379,25 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "criterion"
|
||||
version = "0.8.1"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf"
|
||||
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
|
||||
dependencies = [
|
||||
"alloca",
|
||||
"anes",
|
||||
"cast",
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"itertools 0.13.0",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"oorandom",
|
||||
"page_size",
|
||||
"plotters",
|
||||
"rayon",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"walkdir",
|
||||
|
|
@ -1416,12 +1405,12 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "criterion-plot"
|
||||
version = "0.8.1"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4"
|
||||
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
|
||||
dependencies = [
|
||||
"cast",
|
||||
"itertools 0.13.0",
|
||||
"itertools 0.10.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1437,7 +1426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "51360853ebbeb3df20c76c82aecf43d387a62860f1a59ba65ab51f00eea85aad"
|
||||
dependencies = [
|
||||
"crokey-proc_macros",
|
||||
"crossterm",
|
||||
"crossterm 0.29.0",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"strict",
|
||||
|
|
@ -1449,7 +1438,7 @@ version = "1.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bf1a727caeb5ee5e0a0826a97f205a9cf84ee964b0b48239fef5214a00ae439"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"crossterm 0.29.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strict",
|
||||
|
|
@ -1512,6 +1501,22 @@ version = "0.8.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.28.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"crossterm_winapi",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"rustix 0.38.44",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.29.0"
|
||||
|
|
@ -1555,16 +1560,6 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "csscolorparser"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf"
|
||||
dependencies = [
|
||||
"lab",
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cssparser"
|
||||
version = "0.35.0"
|
||||
|
|
@ -1703,12 +1698,6 @@ version = "2.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
|
||||
|
||||
[[package]]
|
||||
name = "deltae"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.5"
|
||||
|
|
@ -1988,15 +1977,6 @@ version = "3.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
|
||||
|
||||
[[package]]
|
||||
name = "euclid"
|
||||
version = "0.22.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "5.4.1"
|
||||
|
|
@ -2031,16 +2011,6 @@ dependencies = [
|
|||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
|
||||
dependencies = [
|
||||
"bit-set 0.5.3",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.16.2"
|
||||
|
|
@ -2081,17 +2051,6 @@ version = "0.2.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"thiserror 1.0.69",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-crate"
|
||||
version = "0.6.3"
|
||||
|
|
@ -2107,12 +2066,6 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
|
||||
|
||||
[[package]]
|
||||
name = "finl_unicode"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5"
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
|
|
@ -3226,7 +3179,7 @@ checksum = "2628910d0114e9139056161d8644a2026be7b117f8498943f9437748b04c9e0a"
|
|||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"chrono",
|
||||
"crossterm",
|
||||
"crossterm 0.29.0",
|
||||
"dyn-clone",
|
||||
"fuzzy-matcher",
|
||||
"tempfile",
|
||||
|
|
@ -3254,7 +3207,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "8c619cdaa30bb84088963968bee12a45ea5fbbf355f2c021bcd15589f5ca494a"
|
||||
dependencies = [
|
||||
"num_cpus",
|
||||
"ordered-float 3.9.2",
|
||||
"ordered-float",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"rayon",
|
||||
|
|
@ -3323,6 +3276,17 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi 0.5.2",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_ci"
|
||||
version = "1.2.0"
|
||||
|
|
@ -3421,17 +3385,6 @@ dependencies = [
|
|||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kasuari"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fe90c1150662e858c7d5f945089b7517b0a80d8bf7ba4b1b5ffc984e7230a5b"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"portable-atomic",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keccak"
|
||||
version = "0.1.5"
|
||||
|
|
@ -3461,12 +3414,6 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lab"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f"
|
||||
|
||||
[[package]]
|
||||
name = "lalrpop"
|
||||
version = "0.20.2"
|
||||
|
|
@ -3629,15 +3576,6 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "line-clipping"
|
||||
version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linfa-linalg"
|
||||
version = "0.1.0"
|
||||
|
|
@ -3775,16 +3713,6 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mac_address"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303"
|
||||
dependencies = [
|
||||
"nix 0.29.0",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
|
|
@ -3943,21 +3871,6 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memmem"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miette"
|
||||
version = "5.10.0"
|
||||
|
|
@ -4252,7 +4165,6 @@ dependencies = [
|
|||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -4356,7 +4268,7 @@ version = "0.109.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3b777faf7c5180fe5d7f67d83c44fd14138d91f2938a36494ed6ac66b7160f3"
|
||||
dependencies = [
|
||||
"fancy-regex 0.16.2",
|
||||
"fancy-regex",
|
||||
"log",
|
||||
"nu-experimental",
|
||||
"nu-glob",
|
||||
|
|
@ -4451,7 +4363,7 @@ dependencies = [
|
|||
"chrono-humanize",
|
||||
"dirs",
|
||||
"dirs-sys",
|
||||
"fancy-regex 0.16.2",
|
||||
"fancy-regex",
|
||||
"heck 0.5.0",
|
||||
"indexmap 2.12.1",
|
||||
"log",
|
||||
|
|
@ -4506,9 +4418,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "3f8eb43c29cc5bce85f87defdadc2cca964fa434d808af37036a7cb78f3c68e9"
|
||||
dependencies = [
|
||||
"byteyarn",
|
||||
"crossterm",
|
||||
"crossterm 0.29.0",
|
||||
"crossterm_winapi",
|
||||
"fancy-regex 0.16.2",
|
||||
"fancy-regex",
|
||||
"lean_string",
|
||||
"log",
|
||||
"lscolors",
|
||||
|
|
@ -4547,17 +4459,6 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-format"
|
||||
version = "0.4.4"
|
||||
|
|
@ -4597,15 +4498,6 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-core-foundation"
|
||||
version = "0.3.2"
|
||||
|
|
@ -4775,15 +4667,6 @@ dependencies = [
|
|||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.3"
|
||||
|
|
@ -4833,16 +4716,6 @@ version = "4.2.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
|
|
@ -5604,89 +5477,25 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.30.0"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc"
|
||||
dependencies = [
|
||||
"instability",
|
||||
"ratatui-core",
|
||||
"ratatui-crossterm",
|
||||
"ratatui-macros",
|
||||
"ratatui-termwiz",
|
||||
"ratatui-widgets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-core"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293"
|
||||
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cassowary",
|
||||
"compact_str",
|
||||
"hashbrown 0.16.1",
|
||||
"crossterm 0.28.1",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"kasuari",
|
||||
"lru 0.16.2",
|
||||
"strum 0.27.2",
|
||||
"thiserror 2.0.17",
|
||||
"instability",
|
||||
"itertools 0.13.0",
|
||||
"lru 0.12.5",
|
||||
"paste",
|
||||
"strum 0.26.3",
|
||||
"unicode-segmentation",
|
||||
"unicode-truncate",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-crossterm"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossterm",
|
||||
"instability",
|
||||
"ratatui-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-macros"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4"
|
||||
dependencies = [
|
||||
"ratatui-core",
|
||||
"ratatui-widgets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-termwiz"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c"
|
||||
dependencies = [
|
||||
"ratatui-core",
|
||||
"termwiz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-widgets"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"hashbrown 0.16.1",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools 0.14.0",
|
||||
"line-clipping",
|
||||
"ratatui-core",
|
||||
"strum 0.27.2",
|
||||
"time",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rawpointer"
|
||||
version = "0.2.1"
|
||||
|
|
@ -7158,7 +6967,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"fancy-regex 0.16.2",
|
||||
"fancy-regex",
|
||||
"flate2",
|
||||
"fnv",
|
||||
"once_cell",
|
||||
|
|
@ -7484,69 +7293,6 @@ dependencies = [
|
|||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminfo"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"nom 7.1.3",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termios"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termwiz"
|
||||
version = "0.23.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.10.0",
|
||||
"fancy-regex 0.11.0",
|
||||
"filedescriptor",
|
||||
"finl_unicode",
|
||||
"fixedbitset 0.4.2",
|
||||
"hex",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"memmem",
|
||||
"nix 0.29.0",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"ordered-float 4.6.0",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"phf",
|
||||
"sha2",
|
||||
"signal-hook",
|
||||
"siphasher",
|
||||
"terminfo",
|
||||
"termios",
|
||||
"thiserror 1.0.69",
|
||||
"ucd-trie",
|
||||
"unicode-segmentation",
|
||||
"vtparse",
|
||||
"wezterm-bidi",
|
||||
"wezterm-blob-leases",
|
||||
"wezterm-color-types",
|
||||
"wezterm-dynamic",
|
||||
"wezterm-input-types",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
|
|
@ -7614,9 +7360,7 @@ checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
|
|||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"libc",
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
|
|
@ -8116,7 +7860,6 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tokio",
|
||||
"toml 0.9.10+spec-1.1.0",
|
||||
"tracing-subscriber",
|
||||
"typedialog-core",
|
||||
"unic-langid",
|
||||
]
|
||||
|
|
@ -8216,7 +7959,7 @@ dependencies = [
|
|||
"bincode",
|
||||
"chrono",
|
||||
"criterion",
|
||||
"crossterm",
|
||||
"crossterm 0.29.0",
|
||||
"dialoguer",
|
||||
"dirs",
|
||||
"encrypt",
|
||||
|
|
@ -8289,7 +8032,6 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tokio",
|
||||
"toml 0.9.10+spec-1.1.0",
|
||||
"tracing-subscriber",
|
||||
"typedialog-core",
|
||||
"unic-langid",
|
||||
]
|
||||
|
|
@ -8303,7 +8045,6 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tokio",
|
||||
"toml 0.9.10+spec-1.1.0",
|
||||
"tracing-subscriber",
|
||||
"typedialog-core",
|
||||
"unic-langid",
|
||||
]
|
||||
|
|
@ -8437,13 +8178,13 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
|||
|
||||
[[package]]
|
||||
name = "unicode-truncate"
|
||||
version = "2.0.0"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fbf03860ff438702f3910ca5f28f8dac63c1c11e7efb5012b8b175493606330"
|
||||
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf"
|
||||
dependencies = [
|
||||
"itertools 0.13.0",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.0",
|
||||
"unicode-width 0.1.14",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -8546,7 +8287,6 @@ version = "1.19.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a"
|
||||
dependencies = [
|
||||
"atomic",
|
||||
"getrandom 0.3.4",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
|
|
@ -8592,15 +8332,6 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vtparse"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wait-timeout"
|
||||
version = "0.2.1"
|
||||
|
|
@ -8792,78 +8523,6 @@ dependencies = [
|
|||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-bidi"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec"
|
||||
dependencies = [
|
||||
"log",
|
||||
"wezterm-dynamic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-blob-leases"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"mac_address",
|
||||
"sha2",
|
||||
"thiserror 1.0.69",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-color-types"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296"
|
||||
dependencies = [
|
||||
"csscolorparser",
|
||||
"deltae",
|
||||
"lazy_static",
|
||||
"wezterm-dynamic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-dynamic"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ordered-float 4.6.0",
|
||||
"strsim",
|
||||
"thiserror 1.0.69",
|
||||
"wezterm-dynamic-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-dynamic-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-input-types"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"euclid",
|
||||
"lazy_static",
|
||||
"serde",
|
||||
"wezterm-dynamic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wide"
|
||||
version = "0.7.33"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ colored = "3"
|
|||
rpassword = "7.4"
|
||||
|
||||
# TUI Backend (ratatui)
|
||||
ratatui = "0.30"
|
||||
ratatui = "0.29"
|
||||
crossterm = "0.29"
|
||||
atty = "0.2"
|
||||
|
||||
|
|
@ -88,11 +88,11 @@ petgraph = "0.8"
|
|||
surrealdb = { version = "2.4", features = ["kv-mem"] }
|
||||
|
||||
# Misc
|
||||
tempfile = "3.24"
|
||||
tempfile = "3.23"
|
||||
|
||||
# Testing & Benchmarking
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
proptest = "1.9"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
proptest = "1.4"
|
||||
|
||||
# TypeAgent dependencies
|
||||
nickel-lang-core = "0.16"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ async-trait.workspace = true
|
|||
tera = { workspace = true, optional = true }
|
||||
tempfile.workspace = true
|
||||
dirs.workspace = true # For config path resolution
|
||||
tracing.workspace = true # Logging framework
|
||||
|
||||
# i18n (optional)
|
||||
fluent = { workspace = true, optional = true }
|
||||
|
|
@ -51,6 +50,7 @@ axum = { workspace = true, optional = true }
|
|||
tokio = { workspace = true, optional = true }
|
||||
tower = { workspace = true, optional = true }
|
||||
tower-http = { workspace = true, optional = true }
|
||||
tracing = { workspace = true, optional = true }
|
||||
tracing-subscriber = { workspace = true, optional = true }
|
||||
futures = { workspace = true, optional = true }
|
||||
|
||||
|
|
@ -76,7 +76,7 @@ criterion.workspace = true
|
|||
default = ["cli", "i18n", "templates"]
|
||||
cli = ["inquire", "dialoguer", "rpassword"]
|
||||
tui = ["ratatui", "crossterm", "atty"]
|
||||
web = ["axum", "tokio", "tower", "tower-http", "tracing-subscriber", "futures"]
|
||||
web = ["axum", "tokio", "tower", "tower-http", "tracing", "tracing-subscriber", "futures"]
|
||||
i18n = ["fluent", "fluent-bundle", "unic-langid", "sys-locale"]
|
||||
templates = ["tera"]
|
||||
nushell = ["nu-protocol", "nu-plugin"]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use std::hint::black_box;
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use typedialog_core::form_parser::parse_toml;
|
||||
use typedialog_core::nickel::MetadataParser;
|
||||
|
||||
|
|
|
|||
|
|
@ -535,102 +535,11 @@ async fn index_handler(State(state): State<Arc<WebFormState>>) -> impl IntoRespo
|
|||
script.textContent = content;
|
||||
document.body.appendChild(script);
|
||||
}});
|
||||
|
||||
// After form is loaded and scripts executed, filter dependent options
|
||||
// This ensures primary_language only shows languages selected in detected_languages
|
||||
setTimeout(function() {{
|
||||
if (typeof filterDependentOptions === 'function') {{
|
||||
filterDependentOptions();
|
||||
}}
|
||||
}}, 100);
|
||||
}});
|
||||
}});
|
||||
|
||||
// Dynamic options filtering based on data-options-from
|
||||
function filterDependentOptions() {{
|
||||
// First, update all hidden fields for multiselect from their checkboxes
|
||||
document.querySelectorAll('[id^="values_"]').forEach(hiddenField => {{
|
||||
const fieldName = hiddenField.id.replace('values_', '');
|
||||
const checkboxes = document.querySelectorAll('[data-checkbox-group="' + fieldName + '"]:checked');
|
||||
const values = Array.from(checkboxes).map(cb => cb.value);
|
||||
hiddenField.value = values.join(',');
|
||||
}});
|
||||
|
||||
// Find all select fields with data-options-from attribute
|
||||
document.querySelectorAll('select[data-options-from]').forEach(targetSelect => {{
|
||||
const sourceFieldName = targetSelect.getAttribute('data-options-from');
|
||||
|
||||
// Store original options if not already stored
|
||||
if (!targetSelect.dataset.allOptions) {{
|
||||
const allOptions = Array.from(targetSelect.options).map(opt => ({{
|
||||
value: opt.value,
|
||||
text: opt.textContent
|
||||
}}));
|
||||
targetSelect.dataset.allOptions = JSON.stringify(allOptions);
|
||||
}}
|
||||
|
||||
// Find the source field (could be multiselect checkboxes or select)
|
||||
const sourceHiddenField = document.getElementById('values_' + sourceFieldName);
|
||||
const sourceSelect = document.querySelector('select[data-fieldname="' + sourceFieldName + '"]');
|
||||
|
||||
let selectedValues = [];
|
||||
|
||||
if (sourceHiddenField) {{
|
||||
// Multiselect with checkboxes
|
||||
const valuesStr = sourceHiddenField.value;
|
||||
selectedValues = valuesStr ? valuesStr.split(',').map(v => v.trim()) : [];
|
||||
}} else if (sourceSelect) {{
|
||||
// Regular select
|
||||
selectedValues = [sourceSelect.value];
|
||||
}}
|
||||
|
||||
// Get all original options
|
||||
const allOptions = JSON.parse(targetSelect.dataset.allOptions);
|
||||
|
||||
// Filter options to only include those in selectedValues
|
||||
const filteredOptions = allOptions.filter(opt => selectedValues.includes(opt.value));
|
||||
|
||||
// Save current value - check both selected option and current value
|
||||
let currentValue = targetSelect.value;
|
||||
if (!currentValue) {{
|
||||
// If no value selected yet, check for option with 'selected' attribute
|
||||
const selectedOption = targetSelect.querySelector('option[selected]');
|
||||
if (selectedOption) {{
|
||||
currentValue = selectedOption.value;
|
||||
}}
|
||||
}}
|
||||
|
||||
// Clear and repopulate options
|
||||
targetSelect.innerHTML = '';
|
||||
|
||||
// Determine which value should be selected
|
||||
let valueToSelect = '';
|
||||
if (currentValue && filteredOptions.some(opt => opt.value === currentValue)) {{
|
||||
valueToSelect = currentValue;
|
||||
}} else if (filteredOptions.length > 0) {{
|
||||
valueToSelect = filteredOptions[0].value;
|
||||
}}
|
||||
|
||||
// Rebuild options and mark the selected one
|
||||
filteredOptions.forEach(opt => {{
|
||||
const option = document.createElement('option');
|
||||
option.value = opt.value;
|
||||
option.textContent = opt.text;
|
||||
if (opt.value === valueToSelect) {{
|
||||
option.selected = true;
|
||||
}}
|
||||
targetSelect.appendChild(option);
|
||||
}});
|
||||
}});
|
||||
}}
|
||||
|
||||
// Reactive form field updates
|
||||
document.addEventListener('change', function(e) {{
|
||||
// First, handle dynamic options filtering
|
||||
if (e.target.matches('input[type=checkbox][data-checkbox-group], select')) {{
|
||||
filterDependentOptions();
|
||||
}}
|
||||
|
||||
if (e.target.matches('select, input[type=checkbox], input[type=radio]')) {{
|
||||
const form = document.querySelector('form#complete-form');
|
||||
if (!form) return;
|
||||
|
|
@ -650,8 +559,6 @@ async fn index_handler(State(state): State<Arc<WebFormState>>) -> impl IntoRespo
|
|||
const values = Array.from(checkboxes).map(cb => cb.value);
|
||||
hiddenField.value = values.join(',');
|
||||
}});
|
||||
// Re-run option filtering after dynamic update
|
||||
filterDependentOptions();
|
||||
}})
|
||||
.catch(err => console.error('Failed to update form:', err));
|
||||
}}
|
||||
|
|
@ -927,159 +834,6 @@ async fn submit_field_handler(
|
|||
(StatusCode::OK, headers, Json(json!({"success": true})))
|
||||
}
|
||||
|
||||
/// Render success HTML for normal (non-roundtrip) mode
|
||||
#[cfg(feature = "web")]
|
||||
fn render_success_html(results: &HashMap<String, Value>) -> String {
|
||||
let field_count = results.len();
|
||||
|
||||
let fields_html = if field_count > 0 {
|
||||
let mut html = String::from("<div style='margin: 20px 0;'>");
|
||||
html.push_str("<h3 style='margin: 0 0 10px 0;'>📋 Form Results:</h3>");
|
||||
html.push_str("<div style='background: #1e1e1e; padding: 15px; border-radius: 4px; font-family: monospace; max-height: 400px; overflow-y: auto;'>");
|
||||
|
||||
let mut sorted_fields: Vec<_> = results.iter().collect();
|
||||
sorted_fields.sort_by_key(|(k, _)| k.as_str());
|
||||
|
||||
for (field_name, value) in sorted_fields {
|
||||
let value_str = match value {
|
||||
Value::String(s) => s.clone(),
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::Bool(b) => b.to_string(),
|
||||
Value::Array(arr) => {
|
||||
let items: Vec<String> = arr
|
||||
.iter()
|
||||
.map(|v| match v {
|
||||
Value::String(s) => s.clone(),
|
||||
other => other.to_string(),
|
||||
})
|
||||
.collect();
|
||||
format!("[{}]", items.join(", "))
|
||||
}
|
||||
Value::Null => "(empty)".to_string(),
|
||||
Value::Object(_) => "{...}".to_string(),
|
||||
};
|
||||
|
||||
html.push_str(&format!(
|
||||
r#"<div style="margin: 8px 0; padding: 8px; background: #2d2d30; border-radius: 3px;">
|
||||
<div style="color: #dcdcaa; font-weight: bold;">{}</div>
|
||||
<div style="margin-left: 15px; color: #4ec9b0; margin-top: 4px;">{}</div>
|
||||
</div>"#,
|
||||
html_escape(field_name),
|
||||
html_escape(&value_str)
|
||||
));
|
||||
}
|
||||
|
||||
html.push_str("</div></div>");
|
||||
html
|
||||
} else {
|
||||
String::from("<p style='color: #808080;'>No fields submitted</p>")
|
||||
};
|
||||
|
||||
format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Form Submitted Successfully</title>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
margin: 0;
|
||||
padding: 40px 20px;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: #252526;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}}
|
||||
h1 {{
|
||||
color: #4ec9b0;
|
||||
margin: 0 0 10px 0;
|
||||
}}
|
||||
.header {{
|
||||
border-bottom: 2px solid #007acc;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
}}
|
||||
.stat-row {{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: #1e1e1e;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
.actions {{
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #3e3e42;
|
||||
}}
|
||||
.btn {{
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
background: #007acc;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}}
|
||||
.btn:hover {{
|
||||
background: #005a9e;
|
||||
}}
|
||||
.auto-close {{
|
||||
color: #808080;
|
||||
font-size: 12px;
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
}}
|
||||
</style>
|
||||
<script>
|
||||
let countdown = 30;
|
||||
function updateCountdown() {{
|
||||
document.getElementById('countdown').textContent = countdown;
|
||||
countdown--;
|
||||
if (countdown < 0) {{
|
||||
window.close();
|
||||
}}
|
||||
}}
|
||||
setInterval(updateCountdown, 1000);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>✅ Form Submitted Successfully!</h1>
|
||||
</div>
|
||||
|
||||
<div class="stat-row">
|
||||
<span>📊 <strong>Fields Submitted:</strong></span>
|
||||
<span>{} fields</span>
|
||||
</div>
|
||||
|
||||
{}
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn" onclick="window.location.reload()">🔄 Submit Another</button>
|
||||
<button class="btn" onclick="window.close()">❌ Close</button>
|
||||
</div>
|
||||
|
||||
<div class="auto-close">
|
||||
⏱️ This window will auto-close in <span id="countdown">30</span> seconds...
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#,
|
||||
field_count, fields_html
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
async fn submit_complete_form_handler(
|
||||
State(state): State<Arc<WebFormState>>,
|
||||
|
|
@ -1148,16 +902,17 @@ async fn submit_complete_form_handler(
|
|||
.body(Body::from(html))
|
||||
.unwrap()
|
||||
} else {
|
||||
// Normal mode: return HTML success page
|
||||
// Normal mode: return JSON success
|
||||
use axum::body::Body;
|
||||
use axum::response::Response;
|
||||
|
||||
let html = render_success_html(&all_results);
|
||||
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("Content-Type", "text/html; charset=utf-8")
|
||||
.body(Body::from(html))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("HX-Trigger", "formComplete")
|
||||
.body(Body::from(
|
||||
serde_json::to_string(&json!({"success": true})).unwrap(),
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
|
@ -1593,25 +1348,17 @@ fn render_field_for_complete_form(
|
|||
.join("\n")
|
||||
};
|
||||
|
||||
let options_from_attr = if let Some(ref source_field) = field.options_from {
|
||||
format!(" data-options-from=\"{}\"", html_escape(source_field))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
(
|
||||
format!(
|
||||
r#"<div class="field" style="margin-bottom: 15px;">
|
||||
<label style="display: block; margin-bottom: 5px; font-weight: bold;">{}</label>
|
||||
<select name="{}" data-fieldname="{}" {} style="width: 100%; padding: 8px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42; box-sizing: border-box;" {}>
|
||||
<select name="{}" style="width: 100%; padding: 8px; background: #1e1e1e; color: #d4d4d4; border: 1px solid #3e3e42; box-sizing: border-box;" {}>
|
||||
<option value="">-- Select --</option>
|
||||
{}
|
||||
</select>
|
||||
</div>"#,
|
||||
html_escape(&field.prompt),
|
||||
html_escape(&field.name),
|
||||
html_escape(&field.name),
|
||||
options_from_attr,
|
||||
if field.required.unwrap_or(false) {
|
||||
"required"
|
||||
} else {
|
||||
|
|
@ -2142,7 +1889,7 @@ fn render_repeating_group_field(
|
|||
.elements
|
||||
.iter()
|
||||
.filter_map(|element| match element {
|
||||
crate::form_parser::FormElement::Field(f) => Some(f.as_ref()),
|
||||
crate::form_parser::FormElement::Field(f) => Some(f),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -2933,16 +2680,10 @@ fn render_add_item_section(
|
|||
} else {
|
||||
""
|
||||
};
|
||||
let options_from_attr = if let Some(ref source_field) = field.options_from {
|
||||
format!(" data-options-from=\"{}\"", html_escape(source_field))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
html.push_str(&format!(
|
||||
r#"<select class="field-input" data-fieldname="{}"{}{} style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">"#,
|
||||
r#"<select class="field-input" data-fieldname="{}"{} style="width: 100%; padding: 8px; background: #252526; color: #d4d4d4; border: 1px solid #3e3e42; border-radius: 3px;">"#,
|
||||
field.name,
|
||||
required_attr,
|
||||
options_from_attr
|
||||
required_attr
|
||||
));
|
||||
for opt in &field.options {
|
||||
html.push_str(&format!(
|
||||
|
|
|
|||
|
|
@ -188,7 +188,6 @@ mod tests {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: Vec::new(),
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
//! Provides logic for evaluating `when` conditions on form elements.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use super::types::FormDefinition;
|
||||
|
||||
|
|
@ -13,12 +12,11 @@ use super::types::FormDefinition;
|
|||
/// - "enable_prometheus == true" → Some("enable_prometheus")
|
||||
/// - "provider == lxd" → Some("provider")
|
||||
/// - "grafana_port >= 3000" → Some("grafana_port")
|
||||
/// - "rust in detected_languages" → Some("detected_languages")
|
||||
pub(super) fn extract_field_from_condition(condition: &str) -> Option<String> {
|
||||
let condition = condition.trim();
|
||||
|
||||
// String operators: contains, startswith, endswith, in
|
||||
let string_operators = ["contains", "startswith", "endswith", "in"];
|
||||
// String operators: contains, startswith, endswith
|
||||
let string_operators = ["contains", "startswith", "endswith"];
|
||||
for op_str in &string_operators {
|
||||
if let Some(pos) = condition.find(op_str) {
|
||||
let before_ok = pos == 0
|
||||
|
|
@ -35,12 +33,7 @@ pub(super) fn extract_field_from_condition(condition: &str) -> Option<String> {
|
|||
.is_alphanumeric();
|
||||
|
||||
if before_ok && after_ok {
|
||||
// For "in" operator, the field is on the right side
|
||||
let field_name = if *op_str == "in" {
|
||||
condition[pos + op_str.len()..].trim()
|
||||
} else {
|
||||
condition[..pos].trim()
|
||||
};
|
||||
let field_name = condition[..pos].trim();
|
||||
return Some(field_name.to_string());
|
||||
}
|
||||
}
|
||||
|
|
@ -120,19 +113,11 @@ pub fn should_load_fragment(
|
|||
/// - "field_name != value"
|
||||
/// - "field_name contains value"
|
||||
/// - "field_name startswith value"
|
||||
/// - "value in field_name" (array membership)
|
||||
/// - "file_exists(path)" (file existence check)
|
||||
/// - "!file_exists(path)" (negated file existence check)
|
||||
pub fn evaluate_condition(condition: &str, results: &HashMap<String, serde_json::Value>) -> bool {
|
||||
let condition = condition.trim();
|
||||
|
||||
// Check for function calls first (file_exists, etc.)
|
||||
if let Some(result) = evaluate_function_call(condition) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check string operators first (word boundaries)
|
||||
let string_operators = ["contains", "startswith", "endswith", "in"];
|
||||
let string_operators = ["contains", "startswith", "endswith"];
|
||||
for op_str in &string_operators {
|
||||
if let Some(pos) = condition.find(op_str) {
|
||||
// Make sure it's word-bounded (not part of another word)
|
||||
|
|
@ -153,53 +138,19 @@ pub fn evaluate_condition(condition: &str, results: &HashMap<String, serde_json:
|
|||
let left = condition[..pos].trim();
|
||||
let right = condition[pos + op_str.len()..].trim();
|
||||
|
||||
let field_value = results
|
||||
.get(left)
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let field_str = value_to_string(&field_value);
|
||||
let expected = parse_condition_value(right);
|
||||
let expected_str = value_to_string(&expected);
|
||||
|
||||
match *op_str {
|
||||
"in" => {
|
||||
// For "in" operator: "value in array_field"
|
||||
// left = value to search for
|
||||
// right = field name containing array
|
||||
let field_value = results
|
||||
.get(right)
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let search_value = left;
|
||||
|
||||
// Handle array membership check
|
||||
match &field_value {
|
||||
serde_json::Value::Array(arr) => {
|
||||
// Check if any array element matches the search value
|
||||
return arr.iter().any(|v| {
|
||||
let v_str = value_to_string(v);
|
||||
v_str == search_value
|
||||
});
|
||||
}
|
||||
serde_json::Value::String(s) => {
|
||||
// Handle comma-separated string as array (for multiselect defaults)
|
||||
return s
|
||||
.split(',')
|
||||
.map(|item| item.trim())
|
||||
.any(|item| item == search_value);
|
||||
}
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// For other operators: "field_name operator value"
|
||||
let field_value = results
|
||||
.get(left)
|
||||
.cloned()
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let field_str = value_to_string(&field_value);
|
||||
let expected = parse_condition_value(right);
|
||||
let expected_str = value_to_string(&expected);
|
||||
|
||||
match *op_str {
|
||||
"contains" => return field_str.contains(&expected_str),
|
||||
"startswith" => return field_str.starts_with(&expected_str),
|
||||
"endswith" => return field_str.ends_with(&expected_str),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
"contains" => return field_str.contains(&expected_str),
|
||||
"startswith" => return field_str.starts_with(&expected_str),
|
||||
"endswith" => return field_str.ends_with(&expected_str),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -254,44 +205,6 @@ pub fn evaluate_condition(condition: &str, results: &HashMap<String, serde_json:
|
|||
true
|
||||
}
|
||||
|
||||
/// Evaluate function calls in conditions
|
||||
/// Returns Some(bool) if a function call was found and evaluated
|
||||
/// Returns None if no function call was found
|
||||
fn evaluate_function_call(condition: &str) -> Option<bool> {
|
||||
let condition = condition.trim();
|
||||
|
||||
// Handle negation prefix
|
||||
let (is_negated, clean_condition) = if let Some(stripped) = condition.strip_prefix('!') {
|
||||
(true, stripped.trim())
|
||||
} else {
|
||||
(false, condition)
|
||||
};
|
||||
|
||||
// Check for file_exists() function
|
||||
if clean_condition.starts_with("file_exists(") && clean_condition.ends_with(')') {
|
||||
let start = "file_exists(".len();
|
||||
let end = clean_condition.len() - 1;
|
||||
let path_arg = clean_condition[start..end].trim();
|
||||
|
||||
// Remove quotes if present
|
||||
let path = if (path_arg.starts_with('"') && path_arg.ends_with('"'))
|
||||
|| (path_arg.starts_with('\'') && path_arg.ends_with('\''))
|
||||
{
|
||||
&path_arg[1..path_arg.len() - 1]
|
||||
} else {
|
||||
path_arg
|
||||
};
|
||||
|
||||
// Check if file exists
|
||||
let exists = Path::new(path).exists();
|
||||
|
||||
// Apply negation if needed
|
||||
return Some(if is_negated { !exists } else { exists });
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse a value from condition right-hand side
|
||||
fn parse_condition_value(s: &str) -> serde_json::Value {
|
||||
let s = s.trim();
|
||||
|
|
@ -394,206 +307,3 @@ pub fn value_to_string(v: &serde_json::Value) -> String {
|
|||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_in_operator_with_array() {
|
||||
let mut results = HashMap::new();
|
||||
results.insert(
|
||||
"detected_languages".to_string(),
|
||||
serde_json::json!(["rust", "python", "go"]),
|
||||
);
|
||||
|
||||
// Should find rust
|
||||
assert!(evaluate_condition("rust in detected_languages", &results));
|
||||
|
||||
// Should find python
|
||||
assert!(evaluate_condition("python in detected_languages", &results));
|
||||
|
||||
// Should not find javascript
|
||||
assert!(!evaluate_condition(
|
||||
"javascript in detected_languages",
|
||||
&results
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_in_operator_with_comma_separated_string() {
|
||||
let mut results = HashMap::new();
|
||||
results.insert(
|
||||
"detected_languages".to_string(),
|
||||
serde_json::json!("rust,python,go"),
|
||||
);
|
||||
|
||||
// Should find rust (with proper trimming)
|
||||
assert!(evaluate_condition("rust in detected_languages", &results));
|
||||
|
||||
// Should find python
|
||||
assert!(evaluate_condition("python in detected_languages", &results));
|
||||
|
||||
// Should not find javascript
|
||||
assert!(!evaluate_condition(
|
||||
"javascript in detected_languages",
|
||||
&results
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_in_operator_with_spaces() {
|
||||
let mut results = HashMap::new();
|
||||
results.insert(
|
||||
"detected_languages".to_string(),
|
||||
serde_json::json!(["rust", "nushell", "nickel"]),
|
||||
);
|
||||
|
||||
// Should work with spaces around "in"
|
||||
assert!(evaluate_condition("rust in detected_languages", &results));
|
||||
assert!(evaluate_condition(
|
||||
" rust in detected_languages ",
|
||||
&results
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_in_operator_not_found() {
|
||||
let mut results = HashMap::new();
|
||||
results.insert(
|
||||
"detected_languages".to_string(),
|
||||
serde_json::json!(["rust", "python"]),
|
||||
);
|
||||
|
||||
// Should not find when value not in array
|
||||
assert!(!evaluate_condition(
|
||||
"javascript in detected_languages",
|
||||
&results
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_in_operator_with_empty_array() {
|
||||
let mut results = HashMap::new();
|
||||
results.insert("detected_languages".to_string(), serde_json::json!([]));
|
||||
|
||||
// Should not find anything in empty array
|
||||
assert!(!evaluate_condition("rust in detected_languages", &results));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_in_operator_with_missing_field() {
|
||||
let results = HashMap::new();
|
||||
|
||||
// Should not find when field doesn't exist
|
||||
assert!(!evaluate_condition("rust in detected_languages", &results));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_field_from_in_condition() {
|
||||
// For "in" operator, should extract the array field name (right side)
|
||||
assert_eq!(
|
||||
extract_field_from_condition("rust in detected_languages"),
|
||||
Some("detected_languages".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
extract_field_from_condition("python in languages"),
|
||||
Some("languages".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_existing_operators_still_work() {
|
||||
let mut results = HashMap::new();
|
||||
results.insert("enable_feature".to_string(), serde_json::json!(true));
|
||||
results.insert("name".to_string(), serde_json::json!("test"));
|
||||
|
||||
// Existing operators should still work
|
||||
assert!(evaluate_condition("enable_feature == true", &results));
|
||||
assert!(evaluate_condition("name == test", &results));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_exists_with_existing_file() {
|
||||
let results = HashMap::new();
|
||||
|
||||
// Create a temporary file
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let test_file = temp_dir.join("typedialog_test_file.txt");
|
||||
std::fs::write(&test_file, "test content").unwrap();
|
||||
|
||||
// Test with double quotes
|
||||
let condition = format!("file_exists(\"{}\")", test_file.display());
|
||||
assert!(evaluate_condition(&condition, &results));
|
||||
|
||||
// Test with single quotes
|
||||
let condition = format!("file_exists('{}')", test_file.display());
|
||||
assert!(evaluate_condition(&condition, &results));
|
||||
|
||||
// Test without quotes
|
||||
let condition = format!("file_exists({})", test_file.display());
|
||||
assert!(evaluate_condition(&condition, &results));
|
||||
|
||||
// Clean up
|
||||
std::fs::remove_file(&test_file).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_exists_with_nonexistent_file() {
|
||||
let results = HashMap::new();
|
||||
|
||||
// Use a path that definitely doesn't exist
|
||||
let nonexistent = "/tmp/typedialog_nonexistent_file_12345.txt";
|
||||
|
||||
let condition = format!("file_exists(\"{}\")", nonexistent);
|
||||
assert!(!evaluate_condition(&condition, &results));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_exists_negated() {
|
||||
let results = HashMap::new();
|
||||
|
||||
// Create a temporary file
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let test_file = temp_dir.join("typedialog_test_negation.txt");
|
||||
std::fs::write(&test_file, "test").unwrap();
|
||||
|
||||
// Negated: should return false for existing file
|
||||
let condition = format!("!file_exists(\"{}\")", test_file.display());
|
||||
assert!(!evaluate_condition(&condition, &results));
|
||||
|
||||
// Clean up
|
||||
std::fs::remove_file(&test_file).unwrap();
|
||||
|
||||
// After removal, negated should return true
|
||||
let condition = format!("!file_exists(\"{}\")", test_file.display());
|
||||
assert!(evaluate_condition(&condition, &results));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_exists_with_directory() {
|
||||
let results = HashMap::new();
|
||||
|
||||
// Test with an existing directory
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let condition = format!("file_exists(\"{}\")", temp_dir.display());
|
||||
|
||||
// Path::exists() returns true for both files and directories
|
||||
assert!(evaluate_condition(&condition, &results));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_exists_with_relative_path() {
|
||||
let results = HashMap::new();
|
||||
|
||||
// Create a file in current directory
|
||||
std::fs::write("typedialog_test_relative.txt", "test").unwrap();
|
||||
|
||||
let condition = "file_exists(\"typedialog_test_relative.txt\")";
|
||||
assert!(evaluate_condition(condition, &results));
|
||||
|
||||
// Clean up
|
||||
std::fs::remove_file("typedialog_test_relative.txt").unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,41 +174,6 @@ pub fn load_and_execute_from_file(
|
|||
execute_with_base_dir(form, base_dir)
|
||||
}
|
||||
|
||||
/// Filter field options based on options_from reference
|
||||
fn filter_options_from(
|
||||
field: &FieldDefinition,
|
||||
previous_results: &HashMap<String, serde_json::Value>,
|
||||
) -> Vec<super::types::SelectOption> {
|
||||
// If no options_from specified, return all options
|
||||
let Some(ref source_field) = field.options_from else {
|
||||
return field.options.clone();
|
||||
};
|
||||
|
||||
// Get the source field's value
|
||||
let Some(source_value) = previous_results.get(source_field) else {
|
||||
// Source field not found, return all options
|
||||
return field.options.clone();
|
||||
};
|
||||
|
||||
// Extract selected values from source field (could be array or comma-separated string)
|
||||
let selected_values: Vec<String> = match source_value {
|
||||
serde_json::Value::Array(arr) => arr
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect(),
|
||||
serde_json::Value::String(s) => s.split(',').map(|item| item.trim().to_string()).collect(),
|
||||
_ => return field.options.clone(), // Unsupported type, return all
|
||||
};
|
||||
|
||||
// Filter options to only include those in selected_values
|
||||
field
|
||||
.options
|
||||
.iter()
|
||||
.filter(|opt| selected_values.contains(&opt.value))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Execute a single field
|
||||
fn execute_field(
|
||||
field: &FieldDefinition,
|
||||
|
|
@ -267,18 +232,8 @@ fn execute_field(
|
|||
));
|
||||
}
|
||||
let prompt_with_marker = format!("{}{}", field.prompt, required_marker);
|
||||
|
||||
// Filter options based on options_from if specified
|
||||
let filtered_options = filter_options_from(field, _previous_results);
|
||||
|
||||
if filtered_options.is_empty() {
|
||||
return Err(crate::ErrorWrapper::form_parse_failed(format!(
|
||||
"No options available for field '{}'. Check options_from reference.",
|
||||
field.name
|
||||
)));
|
||||
}
|
||||
|
||||
let options = filtered_options
|
||||
let options = field
|
||||
.options
|
||||
.iter()
|
||||
.map(|opt| opt.as_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
|
@ -381,7 +336,7 @@ fn build_element_list(
|
|||
for element in form.elements.iter() {
|
||||
match element {
|
||||
FormElement::Item(item) => {
|
||||
let mut item_clone = item.as_ref().clone();
|
||||
let mut item_clone = item.clone();
|
||||
|
||||
// Handle group type with includes
|
||||
if item.item_type == "group" {
|
||||
|
|
@ -432,15 +387,15 @@ fn build_element_list(
|
|||
// Non-group items get order from position counter (insertion order)
|
||||
item_clone.order = order_counter;
|
||||
order_counter += 1;
|
||||
element_list.push((item_clone.order, FormElement::Item(Box::new(item_clone))));
|
||||
element_list.push((item_clone.order, FormElement::Item(item_clone)));
|
||||
}
|
||||
}
|
||||
FormElement::Field(field) => {
|
||||
let mut field_clone = field.as_ref().clone();
|
||||
let mut field_clone = field.clone();
|
||||
// Assign order based on position counter (insertion order)
|
||||
field_clone.order = order_counter;
|
||||
order_counter += 1;
|
||||
element_list.push((field_clone.order, FormElement::Field(Box::new(field_clone))));
|
||||
element_list.push((field_clone.order, FormElement::Field(field_clone)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -493,7 +448,7 @@ pub fn recompute_visible_elements(
|
|||
.is_none_or(|cond| evaluate_condition(cond, results));
|
||||
|
||||
if should_show {
|
||||
visible_items.push(*item);
|
||||
visible_items.push(item);
|
||||
}
|
||||
}
|
||||
FormElement::Field(field) => {
|
||||
|
|
@ -504,7 +459,7 @@ pub fn recompute_visible_elements(
|
|||
.is_none_or(|cond| evaluate_condition(cond, results));
|
||||
|
||||
if should_show {
|
||||
visible_fields.push(*field);
|
||||
visible_fields.push(field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -575,7 +530,7 @@ pub async fn execute_with_backend_complete(
|
|||
let items: Vec<&DisplayItem> = element_list
|
||||
.iter()
|
||||
.filter_map(|(_, e)| match e {
|
||||
FormElement::Item(item) => Some(item.as_ref()),
|
||||
FormElement::Item(item) => Some(item),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -588,7 +543,7 @@ pub async fn execute_with_backend_complete(
|
|||
if selector_field_names.contains(&field.name) {
|
||||
return None;
|
||||
}
|
||||
Some(field.as_ref())
|
||||
Some(field)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
|
|
@ -840,7 +795,7 @@ pub async fn execute_with_backend_i18n_with_defaults(
|
|||
return None;
|
||||
}
|
||||
}
|
||||
Some(item.as_ref())
|
||||
Some(item)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
|
|
@ -857,7 +812,7 @@ pub async fn execute_with_backend_i18n_with_defaults(
|
|||
return None;
|
||||
}
|
||||
}
|
||||
Some(field.as_ref())
|
||||
Some(field)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -30,15 +30,15 @@ where
|
|||
/// Public enum for unified form structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum FormElement {
|
||||
Item(Box<DisplayItem>),
|
||||
Field(Box<FieldDefinition>),
|
||||
Item(DisplayItem),
|
||||
Field(FieldDefinition),
|
||||
}
|
||||
|
||||
impl FormElement {
|
||||
/// Get as DisplayItem if this is an Item variant
|
||||
pub fn as_item(&self) -> Option<&DisplayItem> {
|
||||
match self {
|
||||
FormElement::Item(item) => Some(item.as_ref()),
|
||||
FormElement::Item(item) => Some(item),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ impl FormElement {
|
|||
/// Get mutable reference as DisplayItem if this is an Item variant
|
||||
pub fn as_item_mut(&mut self) -> Option<&mut DisplayItem> {
|
||||
match self {
|
||||
FormElement::Item(item) => Some(item.as_mut()),
|
||||
FormElement::Item(item) => Some(item),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ impl FormElement {
|
|||
/// Get as FieldDefinition if this is a Field variant
|
||||
pub fn as_field(&self) -> Option<&FieldDefinition> {
|
||||
match self {
|
||||
FormElement::Field(field) => Some(field.as_ref()),
|
||||
FormElement::Field(field) => Some(field),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -62,7 +62,7 @@ impl FormElement {
|
|||
/// Get mutable reference as FieldDefinition if this is a Field variant
|
||||
pub fn as_field_mut(&mut self) -> Option<&mut FieldDefinition> {
|
||||
match self {
|
||||
FormElement::Field(field) => Some(field.as_mut()),
|
||||
FormElement::Field(field) => Some(field),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -158,12 +158,12 @@ impl<'de> Deserialize<'de> for FormElement {
|
|||
let item: DisplayItem =
|
||||
serde_json::from_value(serde_json::Value::Object(fields_map))
|
||||
.map_err(de::Error::custom)?;
|
||||
Ok(FormElement::Item(Box::new(item)))
|
||||
Ok(FormElement::Item(item))
|
||||
} else if field_types.contains(&element_type.as_str()) {
|
||||
let field: FieldDefinition =
|
||||
serde_json::from_value(serde_json::Value::Object(fields_map))
|
||||
.map_err(de::Error::custom)?;
|
||||
Ok(FormElement::Field(Box::new(field)))
|
||||
Ok(FormElement::Field(field))
|
||||
} else {
|
||||
Err(de::Error::custom(format!(
|
||||
"Unknown element type '{}'. Item types: {}. Field types: {}",
|
||||
|
|
@ -300,14 +300,14 @@ impl FormDefinition {
|
|||
for mut item in self.items.drain(..) {
|
||||
// Assign order based on position to preserve insertion order
|
||||
item.order = element_list.len();
|
||||
element_list.push(FormElement::Item(Box::new(item)));
|
||||
element_list.push(FormElement::Item(item));
|
||||
}
|
||||
|
||||
// Add fields, preserving insertion order after items
|
||||
for mut field in self.fields.drain(..) {
|
||||
// Assign order based on position to preserve insertion order
|
||||
field.order = element_list.len();
|
||||
element_list.push(FormElement::Field(Box::new(field)));
|
||||
element_list.push(FormElement::Field(field));
|
||||
}
|
||||
|
||||
// Assign to elements (already in correct insertion order)
|
||||
|
|
@ -325,10 +325,10 @@ impl FormDefinition {
|
|||
for element in self.elements.drain(..) {
|
||||
match element {
|
||||
FormElement::Field(field) => {
|
||||
self.fields.push(*field);
|
||||
self.fields.push(field);
|
||||
}
|
||||
FormElement::Item(item) => {
|
||||
self.items.push(*item);
|
||||
self.items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -436,11 +436,6 @@ pub struct FieldDefinition {
|
|||
/// Optional options list with value/label (can contain literal text or i18n keys)
|
||||
#[serde(default)]
|
||||
pub options: Vec<SelectOption>,
|
||||
/// Optional reference to another field whose values should filter this field's options
|
||||
/// Example: options_from = "detected_languages" means only show options whose values
|
||||
/// are present in the detected_languages field's selected values
|
||||
#[serde(default)]
|
||||
pub options_from: Option<String>,
|
||||
/// Optional field requirement flag
|
||||
pub required: Option<bool>,
|
||||
/// Optional file extension (for editor)
|
||||
|
|
|
|||
|
|
@ -408,7 +408,6 @@ mod tests {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -463,7 +462,6 @@ mod tests {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -518,7 +516,6 @@ mod tests {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -574,7 +571,6 @@ mod tests {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -613,7 +609,6 @@ mod tests {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -664,7 +659,6 @@ mod tests {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
|
|||
|
|
@ -100,18 +100,13 @@ impl ContractParser {
|
|||
) {
|
||||
// Could be string, array, object, number, true, false, null
|
||||
// Skip parsing as validator call
|
||||
tracing::debug!(
|
||||
field = %field_name,
|
||||
value = %right_side,
|
||||
"Skipping literal field (not a validator call)"
|
||||
eprintln!(
|
||||
"[DEBUG] Skipping literal for field '{}': '{}'",
|
||||
field_name, right_side
|
||||
);
|
||||
continue;
|
||||
}
|
||||
tracing::debug!(
|
||||
field = %field_name,
|
||||
expression = %right_side,
|
||||
"Parsing field with validator"
|
||||
);
|
||||
eprintln!("[DEBUG] Parsing field '{}': '{}'", field_name, right_side);
|
||||
|
||||
// Extract module.function pairs from right side
|
||||
if let Some(dot_pos) = right_side.find('.') {
|
||||
|
|
|
|||
|
|
@ -55,15 +55,6 @@ pub struct RoundtripResult {
|
|||
|
||||
/// Validation result (if enabled)
|
||||
pub validation_passed: Option<bool>,
|
||||
|
||||
/// Original input Nickel code (for diff)
|
||||
pub input_nickel: String,
|
||||
|
||||
/// Path to output file
|
||||
pub output_path: PathBuf,
|
||||
|
||||
/// Initial values loaded from input (for change detection)
|
||||
pub initial_values: std::collections::HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl RoundtripConfig {
|
||||
|
|
@ -127,26 +118,12 @@ impl RoundtripConfig {
|
|||
);
|
||||
}
|
||||
|
||||
// Step 2: Load defaults from input .ncl (if fields have nickel_path)
|
||||
let initial_values =
|
||||
Self::load_defaults_from_input(&self.input_ncl, &self.form_path, self.verbose)?;
|
||||
|
||||
if self.verbose && !initial_values.is_empty() {
|
||||
eprintln!(
|
||||
"[roundtrip] Loaded {} default values from input",
|
||||
initial_values.len()
|
||||
);
|
||||
}
|
||||
|
||||
// Step 3: Execute form to get results (with defaults pre-populated)
|
||||
// Step 2: Execute form to get results
|
||||
if self.verbose {
|
||||
eprintln!("[roundtrip] Executing form: {}", self.form_path.display());
|
||||
}
|
||||
|
||||
let initial_values_backup = initial_values.clone();
|
||||
let form_results =
|
||||
Self::execute_form_with_backend_and_defaults(&self.form_path, backend, initial_values)
|
||||
.await?;
|
||||
let form_results = Self::execute_form_with_backend(&self.form_path, backend).await?;
|
||||
|
||||
if self.verbose {
|
||||
eprintln!(
|
||||
|
|
@ -220,11 +197,8 @@ impl RoundtripConfig {
|
|||
Ok(RoundtripResult {
|
||||
input_contracts,
|
||||
form_results,
|
||||
output_nickel: output_nickel.clone(),
|
||||
output_nickel,
|
||||
validation_passed,
|
||||
input_nickel: input_source,
|
||||
output_path: self.output_ncl,
|
||||
initial_values: initial_values_backup,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -256,24 +230,12 @@ impl RoundtripConfig {
|
|||
);
|
||||
}
|
||||
|
||||
// Step 2: Load defaults from input .ncl (if fields have nickel_path)
|
||||
let initial_values =
|
||||
Self::load_defaults_from_input(&self.input_ncl, &self.form_path, self.verbose)?;
|
||||
|
||||
if self.verbose && !initial_values.is_empty() {
|
||||
eprintln!(
|
||||
"[roundtrip] Loaded {} default values from input",
|
||||
initial_values.len()
|
||||
);
|
||||
}
|
||||
|
||||
// Step 3: Execute form to get results (with defaults pre-populated)
|
||||
// Step 2: Execute form to get results
|
||||
if self.verbose {
|
||||
eprintln!("[roundtrip] Executing form: {}", self.form_path.display());
|
||||
}
|
||||
|
||||
let initial_values_backup = initial_values.clone();
|
||||
let form_results = Self::execute_form_with_defaults(&self.form_path, initial_values)?;
|
||||
let form_results = Self::execute_form(&self.form_path)?;
|
||||
|
||||
if self.verbose {
|
||||
eprintln!(
|
||||
|
|
@ -347,19 +309,15 @@ impl RoundtripConfig {
|
|||
Ok(RoundtripResult {
|
||||
input_contracts,
|
||||
form_results,
|
||||
output_nickel: output_nickel.clone(),
|
||||
output_nickel,
|
||||
validation_passed,
|
||||
input_nickel: input_source,
|
||||
output_path: self.output_ncl,
|
||||
initial_values: initial_values_backup,
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute a form with a specific backend and return results (with defaults)
|
||||
async fn execute_form_with_backend_and_defaults(
|
||||
/// Execute a form with a specific backend and return results
|
||||
async fn execute_form_with_backend(
|
||||
form_path: &Path,
|
||||
backend: &mut dyn FormBackend,
|
||||
initial_values: HashMap<String, Value>,
|
||||
) -> Result<HashMap<String, Value>> {
|
||||
// Read form definition
|
||||
let form_content = fs::read_to_string(form_path).map_err(|e| {
|
||||
|
|
@ -372,418 +330,23 @@ impl RoundtripConfig {
|
|||
// Migrate to unified elements format if needed
|
||||
form.migrate_to_elements();
|
||||
|
||||
// NOTE: We don't apply defaults here because execute_with_backend_two_phase_with_defaults
|
||||
// will call build_element_list which reloads fragments from disk, losing any modifications.
|
||||
// Instead, we pass initial_values to execute_with_backend_two_phase_with_defaults
|
||||
// which will apply them after fragment expansion.
|
||||
// Extract base directory for resolving relative paths (includes, fragments)
|
||||
let base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
|
||||
|
||||
// Execute form using provided backend (TUI, Web, or CLI)
|
||||
form_parser::execute_with_backend_two_phase(form, backend, None, base_dir).await
|
||||
}
|
||||
|
||||
/// Execute a form and return results (CLI backend only)
|
||||
fn execute_form(form_path: &Path) -> Result<HashMap<String, Value>> {
|
||||
// Load form definition from file (resolves constraint interpolations + includes)
|
||||
let form = form_parser::load_from_file(form_path)?;
|
||||
|
||||
// Extract base directory for resolving relative paths (includes, fragments)
|
||||
let base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
|
||||
|
||||
// Execute form using provided backend (TUI, Web, or CLI) with defaults
|
||||
form_parser::execute_with_backend_two_phase_with_defaults(
|
||||
form,
|
||||
backend,
|
||||
None,
|
||||
base_dir,
|
||||
Some(initial_values),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Execute a form and return results (CLI backend only, with defaults)
|
||||
fn execute_form_with_defaults(
|
||||
form_path: &Path,
|
||||
initial_values: HashMap<String, Value>,
|
||||
) -> Result<HashMap<String, Value>> {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// Read form definition
|
||||
let form_content = fs::read_to_string(form_path).map_err(|e| {
|
||||
crate::error::ErrorWrapper::new(format!("Failed to read form file: {}", e))
|
||||
})?;
|
||||
|
||||
// Parse TOML form definition
|
||||
let mut form = form_parser::parse_toml(&form_content)?;
|
||||
|
||||
// Migrate to unified elements format
|
||||
form.migrate_to_elements();
|
||||
|
||||
// Extract base directory for resolving relative paths (includes, fragments)
|
||||
let base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
|
||||
|
||||
// CRITICAL FIX: Expand fragments BEFORE applying defaults
|
||||
// This ensures defaults are applied to real fields, not to groups with includes
|
||||
let mut expanded_form = form_parser::expand_includes(form, base_dir)?;
|
||||
|
||||
// Now apply initial values as defaults to the EXPANDED fields
|
||||
for element in &mut expanded_form.elements {
|
||||
if let form_parser::FormElement::Field(field) = element {
|
||||
if let Some(value) = initial_values.get(&field.name) {
|
||||
// Set as default if field doesn't already have one
|
||||
if field.default.is_none() {
|
||||
field.default = Some(form_parser::value_to_string(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Execute expanded form directly (without re-expanding includes)
|
||||
// This is a simplified version of execute_with_base_dir that doesn't call expand_includes again
|
||||
let mut results = HashMap::new();
|
||||
|
||||
// Print form header
|
||||
if let Some(desc) = &expanded_form.description {
|
||||
println!("\n{}\n{}\n", expanded_form.name, desc);
|
||||
} else {
|
||||
println!("\n{}\n", expanded_form.name);
|
||||
}
|
||||
|
||||
// Build ordered element map
|
||||
let mut element_map: BTreeMap<usize, form_parser::FormElement> = BTreeMap::new();
|
||||
let mut order_counter = 0;
|
||||
|
||||
for element in expanded_form.elements {
|
||||
let order = match &element {
|
||||
form_parser::FormElement::Item(item) => {
|
||||
if item.order == 0 {
|
||||
order_counter += 1;
|
||||
order_counter - 1
|
||||
} else {
|
||||
item.order
|
||||
}
|
||||
}
|
||||
form_parser::FormElement::Field(field) => {
|
||||
if field.order == 0 {
|
||||
order_counter += 1;
|
||||
order_counter - 1
|
||||
} else {
|
||||
field.order
|
||||
}
|
||||
}
|
||||
};
|
||||
element_map.insert(order, element);
|
||||
}
|
||||
|
||||
// Process elements in order (using CLI prompts)
|
||||
for (_, element) in element_map.iter() {
|
||||
match element {
|
||||
form_parser::FormElement::Item(item) => {
|
||||
form_parser::render_display_item(item, &results);
|
||||
}
|
||||
form_parser::FormElement::Field(field) => {
|
||||
// Check if field should be shown based on conditional
|
||||
if let Some(condition) = &field.when {
|
||||
if !form_parser::evaluate_condition(condition, &results) {
|
||||
// Field condition not met, skip it
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute field using CLI prompts (from execute_with_base_dir logic)
|
||||
let value = Self::execute_field_cli(field, &results)?;
|
||||
results.insert(field.name.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Execute a single field using CLI prompts (extracted from executor.rs)
|
||||
fn execute_field_cli(
|
||||
field: &form_parser::FieldDefinition,
|
||||
previous_results: &HashMap<String, Value>,
|
||||
) -> Result<Value> {
|
||||
use crate::prompts;
|
||||
|
||||
let is_required = field.required.unwrap_or(false);
|
||||
let required_marker = if is_required { " *" } else { " (optional)" };
|
||||
|
||||
match field.field_type {
|
||||
form_parser::FieldType::Text => {
|
||||
let prompt_with_marker = format!("{}{}", field.prompt, required_marker);
|
||||
let result = prompts::text(
|
||||
&prompt_with_marker,
|
||||
field.default.as_deref(),
|
||||
field.placeholder.as_deref(),
|
||||
)?;
|
||||
|
||||
if is_required && result.is_empty() {
|
||||
eprintln!("⚠ This field is required. Please enter a value.");
|
||||
return Self::execute_field_cli(field, previous_results); // Retry
|
||||
}
|
||||
Ok(serde_json::json!(result))
|
||||
}
|
||||
|
||||
form_parser::FieldType::Confirm => {
|
||||
let prompt_with_marker = format!("{}{}", field.prompt, required_marker);
|
||||
let default_bool =
|
||||
field
|
||||
.default
|
||||
.as_deref()
|
||||
.and_then(|s| match s.to_lowercase().as_str() {
|
||||
"true" | "yes" => Some(true),
|
||||
"false" | "no" => Some(false),
|
||||
_ => None,
|
||||
});
|
||||
let result = prompts::confirm(&prompt_with_marker, default_bool, None)?;
|
||||
Ok(serde_json::json!(result))
|
||||
}
|
||||
|
||||
form_parser::FieldType::Password => {
|
||||
let prompt_with_marker = format!("{}{}", field.prompt, required_marker);
|
||||
let with_toggle = field.placeholder.as_deref() == Some("toggle");
|
||||
let result = prompts::password(&prompt_with_marker, with_toggle)?;
|
||||
|
||||
if is_required && result.is_empty() {
|
||||
eprintln!("⚠ This field is required. Please enter a value.");
|
||||
return Self::execute_field_cli(field, previous_results); // Retry
|
||||
}
|
||||
Ok(serde_json::json!(result))
|
||||
}
|
||||
|
||||
form_parser::FieldType::Select => {
|
||||
if field.options.is_empty() {
|
||||
return Err(crate::error::ErrorWrapper::new(
|
||||
"Select field requires 'options'".to_string(),
|
||||
));
|
||||
}
|
||||
let prompt_with_marker = format!("{}{}", field.prompt, required_marker);
|
||||
|
||||
// Filter options based on options_from if specified
|
||||
let filtered_options = Self::filter_options_from(field, previous_results);
|
||||
|
||||
if filtered_options.is_empty() {
|
||||
return Err(crate::error::ErrorWrapper::new(format!(
|
||||
"No options available for field '{}'. Check options_from reference.",
|
||||
field.name
|
||||
)));
|
||||
}
|
||||
|
||||
let options = filtered_options
|
||||
.iter()
|
||||
.map(|opt| opt.as_string())
|
||||
.collect::<Vec<_>>();
|
||||
let result = prompts::select(
|
||||
&prompt_with_marker,
|
||||
options,
|
||||
field.page_size,
|
||||
field.vim_mode.unwrap_or(false),
|
||||
)?;
|
||||
Ok(serde_json::json!(result))
|
||||
}
|
||||
|
||||
form_parser::FieldType::MultiSelect => {
|
||||
if field.options.is_empty() {
|
||||
return Err(crate::error::ErrorWrapper::new(
|
||||
"MultiSelect field requires 'options'".to_string(),
|
||||
));
|
||||
}
|
||||
let prompt_with_marker = format!("{}{}", field.prompt, required_marker);
|
||||
let options = field
|
||||
.options
|
||||
.iter()
|
||||
.map(|opt| opt.as_string())
|
||||
.collect::<Vec<_>>();
|
||||
let results = prompts::multi_select(
|
||||
&prompt_with_marker,
|
||||
options,
|
||||
field.page_size,
|
||||
field.vim_mode.unwrap_or(false),
|
||||
)?;
|
||||
|
||||
if is_required && results.is_empty() {
|
||||
eprintln!("⚠ This field is required. Please select at least one option.");
|
||||
return Self::execute_field_cli(field, previous_results); // Retry
|
||||
}
|
||||
Ok(serde_json::json!(results))
|
||||
}
|
||||
|
||||
form_parser::FieldType::Editor => {
|
||||
let prompt_with_marker = format!("{}{}", field.prompt, required_marker);
|
||||
let result = prompts::editor(
|
||||
&prompt_with_marker,
|
||||
field.file_extension.as_deref(),
|
||||
field.prefix_text.as_deref(),
|
||||
)?;
|
||||
|
||||
if is_required && result.is_empty() {
|
||||
eprintln!("⚠ This field is required. Please enter a value.");
|
||||
return Self::execute_field_cli(field, previous_results); // Retry
|
||||
}
|
||||
Ok(serde_json::json!(result))
|
||||
}
|
||||
|
||||
form_parser::FieldType::Date => {
|
||||
let prompt_with_marker = format!("{}{}", field.prompt, required_marker);
|
||||
let week_start = field.week_start.as_deref().unwrap_or("Mon");
|
||||
let result = prompts::date(
|
||||
&prompt_with_marker,
|
||||
field.default.as_deref(),
|
||||
field.min_date.as_deref(),
|
||||
field.max_date.as_deref(),
|
||||
week_start,
|
||||
)?;
|
||||
Ok(serde_json::json!(result))
|
||||
}
|
||||
|
||||
form_parser::FieldType::Custom => {
|
||||
let prompt_with_marker = format!("{}{}", field.prompt, required_marker);
|
||||
let type_name = field.custom_type.as_ref().ok_or_else(|| {
|
||||
crate::error::ErrorWrapper::new(
|
||||
"Custom field requires 'custom_type'".to_string(),
|
||||
)
|
||||
})?;
|
||||
let result =
|
||||
prompts::custom(&prompt_with_marker, type_name, field.default.as_deref())?;
|
||||
|
||||
if is_required && result.is_empty() {
|
||||
eprintln!("⚠ This field is required. Please enter a value.");
|
||||
return Self::execute_field_cli(field, previous_results); // Retry
|
||||
}
|
||||
Ok(serde_json::json!(result))
|
||||
}
|
||||
|
||||
form_parser::FieldType::RepeatingGroup => Err(crate::error::ErrorWrapper::new(
|
||||
"RepeatingGroup not yet implemented".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Filter field options based on options_from reference (extracted from executor.rs)
|
||||
fn filter_options_from(
|
||||
field: &form_parser::FieldDefinition,
|
||||
previous_results: &HashMap<String, Value>,
|
||||
) -> Vec<form_parser::SelectOption> {
|
||||
// If no options_from specified, return all options
|
||||
let Some(ref source_field) = field.options_from else {
|
||||
return field.options.clone();
|
||||
};
|
||||
|
||||
// Get the source field's value
|
||||
let Some(source_value) = previous_results.get(source_field) else {
|
||||
// Source field not found, return all options
|
||||
return field.options.clone();
|
||||
};
|
||||
|
||||
// Extract selected values from source field (could be array or comma-separated string)
|
||||
let selected_values: Vec<String> = match source_value {
|
||||
Value::Array(arr) => arr
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect(),
|
||||
Value::String(s) => s.split(',').map(|item| item.trim().to_string()).collect(),
|
||||
_ => return field.options.clone(), // Unsupported type, return all
|
||||
};
|
||||
|
||||
// Filter options to only include those in selected_values
|
||||
field
|
||||
.options
|
||||
.iter()
|
||||
.filter(|opt| selected_values.contains(&opt.value))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Load defaults from input Nickel file using form field nickel_path
|
||||
fn load_defaults_from_input(
|
||||
input_path: &Path,
|
||||
form_path: &Path,
|
||||
verbose: bool,
|
||||
) -> Result<HashMap<String, Value>> {
|
||||
// Export input .ncl to JSON
|
||||
let json_value = match NickelCli::export(input_path) {
|
||||
Ok(val) => val,
|
||||
Err(_) => {
|
||||
// If export fails (e.g., file is empty or has syntax errors),
|
||||
// return empty defaults rather than failing the whole roundtrip
|
||||
if verbose {
|
||||
eprintln!(
|
||||
"[roundtrip] Warning: Could not export input .ncl, proceeding without defaults"
|
||||
);
|
||||
}
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
};
|
||||
|
||||
// Load form to get field definitions with nickel_path
|
||||
let form_content = fs::read_to_string(form_path).map_err(|e| {
|
||||
crate::error::ErrorWrapper::new(format!("Failed to read form file: {}", e))
|
||||
})?;
|
||||
|
||||
let mut form = form_parser::parse_toml(&form_content)?;
|
||||
form.migrate_to_elements();
|
||||
|
||||
// Extract base directory for resolving fragment includes
|
||||
let base_dir = form_path.parent().unwrap_or_else(|| Path::new("."));
|
||||
|
||||
// Expand fragments to get ALL fields (including those in conditional groups)
|
||||
// Uses expand_includes to process group elements with includes
|
||||
let expanded_form = form_parser::expand_includes(form, base_dir)?;
|
||||
|
||||
// Extract field definitions that have nickel_path
|
||||
let fields_with_paths: Vec<_> = expanded_form
|
||||
.elements
|
||||
.iter()
|
||||
.filter_map(|elem| {
|
||||
if let form_parser::FormElement::Field(field) = elem {
|
||||
if field.nickel_path.is_some() {
|
||||
Some(field.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if fields_with_paths.is_empty() {
|
||||
// No fields with nickel_path, return empty
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
// Extract values using nickel_path
|
||||
let mut defaults = HashMap::new();
|
||||
|
||||
if let Value::Object(obj) = json_value {
|
||||
for field in &fields_with_paths {
|
||||
if let Some(nickel_path) = &field.nickel_path {
|
||||
if let Some(value) =
|
||||
Self::extract_value_by_path(&Value::Object(obj.clone()), nickel_path)
|
||||
{
|
||||
defaults.insert(field.name.clone(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if verbose && !defaults.is_empty() {
|
||||
eprintln!(
|
||||
"[roundtrip] Extracted {} default values from {} fields with nickel_path",
|
||||
defaults.len(),
|
||||
fields_with_paths.len()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(defaults)
|
||||
}
|
||||
|
||||
/// Extract a value from nested JSON using a path
|
||||
fn extract_value_by_path(json: &Value, path: &[String]) -> Option<Value> {
|
||||
let mut current = json;
|
||||
|
||||
for key in path {
|
||||
match current {
|
||||
Value::Object(map) => {
|
||||
current = map.get(key)?;
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
|
||||
Some(current.clone())
|
||||
// Execute form using CLI backend (interactive prompts)
|
||||
form_parser::execute_with_base_dir(form, base_dir)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -804,39 +367,4 @@ mod tests {
|
|||
assert_eq!(config.output_ncl, PathBuf::from("output.ncl"));
|
||||
assert!(config.validate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_value_by_path() {
|
||||
use serde_json::json;
|
||||
|
||||
let json = json!({
|
||||
"ci": {
|
||||
"project": {
|
||||
"name": "test-project"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let path = vec!["ci".to_string(), "project".to_string(), "name".to_string()];
|
||||
let result = RoundtripConfig::extract_value_by_path(&json, &path);
|
||||
|
||||
assert!(result.is_some());
|
||||
assert_eq!(result.unwrap(), json!("test-project"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_value_by_path_missing() {
|
||||
use serde_json::json;
|
||||
|
||||
let json = json!({
|
||||
"ci": {
|
||||
"project": {}
|
||||
}
|
||||
});
|
||||
|
||||
let path = vec!["ci".to_string(), "project".to_string(), "name".to_string()];
|
||||
let result = RoundtripConfig::extract_value_by_path(&json, &path);
|
||||
|
||||
assert!(result.is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,25 +96,28 @@ impl RoundtripSummary {
|
|||
|
||||
output.push('\n');
|
||||
output.push_str("╔════════════════════════════════════════════════════════════╗\n");
|
||||
output.push_str(" ✅ Configuration Saved Successfully!\n");
|
||||
output.push_str("════════════════════════════════════════════════════════════\n");
|
||||
output.push_str(&format!(" 📄 File: {}\n", self.output_path));
|
||||
output.push_str("║ ✅ Configuration Saved Successfully! ║\n");
|
||||
output.push_str("╠════════════════════════════════════════════════════════════╣\n");
|
||||
output.push_str(&format!(
|
||||
"║ 📄 File: {:<48} ║\n",
|
||||
truncate(&self.output_path, 48)
|
||||
));
|
||||
|
||||
if let Some(passed) = self.validation_passed {
|
||||
let status = if passed { "✓ PASSED" } else { "✗ FAILED" };
|
||||
output.push_str(&format!(" ✓ Validation: {}\n", status));
|
||||
output.push_str(&format!("║ ✓ Validation: {:<43} ║\n", status));
|
||||
}
|
||||
|
||||
output.push_str(&format!(
|
||||
" 📊 Fields: {}/{} changed, {} unchanged\n",
|
||||
self.changed_fields, self.total_fields, self.unchanged_fields
|
||||
"║ 📊 Fields: {}/{} changed, {} unchanged{:<18} ║\n",
|
||||
self.changed_fields, self.total_fields, self.unchanged_fields, ""
|
||||
));
|
||||
|
||||
output.push_str("════════════════════════════════════════════════════════════\n");
|
||||
output.push_str("╠════════════════════════════════════════════════════════════╣\n");
|
||||
|
||||
// Show changes
|
||||
if self.changed_fields > 0 {
|
||||
output.push_str(" 📋 What Changed:\n");
|
||||
output.push_str("║ 📋 What Changed: ║\n");
|
||||
|
||||
let mut shown = 0;
|
||||
for change in &self.changes {
|
||||
|
|
@ -125,29 +128,30 @@ impl RoundtripSummary {
|
|||
if !verbose && shown >= 10 {
|
||||
let remaining = self.changed_fields - shown;
|
||||
output.push_str(&format!(
|
||||
" ... and {} more changes (use --verbose to see all)\n",
|
||||
remaining
|
||||
"║ ... and {} more changes (use --verbose to see all){:<4} ║\n",
|
||||
remaining, ""
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
output.push_str(&format!(
|
||||
" ├─ {}: {} → {}\n",
|
||||
let line = format!(
|
||||
" ├─ {}: {} → {}",
|
||||
change.field_name,
|
||||
truncate(&change.old_value, 20),
|
||||
truncate(&change.new_value, 20)
|
||||
));
|
||||
truncate(&change.old_value, 15),
|
||||
truncate(&change.new_value, 15)
|
||||
);
|
||||
output.push_str(&format!("║ {:<58} ║\n", truncate(&line, 58)));
|
||||
shown += 1;
|
||||
}
|
||||
} else {
|
||||
output.push_str(" 📋 No changes made\n");
|
||||
output.push_str("║ 📋 No changes made ║\n");
|
||||
}
|
||||
|
||||
output.push_str("════════════════════════════════════════════════════════════\n");
|
||||
output.push_str(" 💡 Next Steps:\n");
|
||||
output.push_str(" • Review: cat config.ncl\n");
|
||||
output.push_str(" • Apply CI tools: ./setup-ci.sh\n");
|
||||
output.push_str(" • Re-configure: ./ci-configure.sh\n");
|
||||
output.push_str("╠════════════════════════════════════════════════════════════╣\n");
|
||||
output.push_str("║ 💡 Next Steps: ║\n");
|
||||
output.push_str("║ • Review: cat config.ncl ║\n");
|
||||
output.push_str("║ • Apply CI tools: ./setup-ci.sh ║\n");
|
||||
output.push_str("║ • Re-configure: ./ci-configure.sh ║\n");
|
||||
output.push_str("╚════════════════════════════════════════════════════════════╝\n");
|
||||
output.push('\n');
|
||||
|
||||
|
|
|
|||
|
|
@ -316,7 +316,6 @@ impl TomlGenerator {
|
|||
default,
|
||||
placeholder: None,
|
||||
options,
|
||||
options_from: None,
|
||||
required,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -475,7 +474,6 @@ impl TomlGenerator {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: Vec::new(),
|
||||
options_from: None,
|
||||
required: Some(!field.optional),
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ mod encryption_tests {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -129,7 +128,6 @@ mod encryption_tests {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -187,7 +185,6 @@ mod encryption_tests {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -244,7 +241,6 @@ mod encryption_tests {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -339,7 +335,6 @@ mod age_roundtrip_tests {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
|
|||
|
|
@ -1200,7 +1200,6 @@ fn test_encryption_roundtrip_with_redaction() {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -1239,7 +1238,6 @@ fn test_encryption_roundtrip_with_redaction() {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -1278,7 +1276,6 @@ fn test_encryption_roundtrip_with_redaction() {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -1355,7 +1352,6 @@ fn test_encryption_auto_detection_from_field_type() {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -1420,7 +1416,6 @@ fn test_sensitive_field_explicit_override() {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
@ -1491,7 +1486,6 @@ fn test_mixed_sensitive_and_non_sensitive_fields() {
|
|||
default: None,
|
||||
placeholder: None,
|
||||
options: vec![],
|
||||
options_from: None,
|
||||
required: None,
|
||||
file_extension: None,
|
||||
prefix_text: None,
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ tokio = { workspace = true, features = ["rt-multi-thread"] }
|
|||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
unic-langid = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -181,33 +181,23 @@ pub async fn nickel_roundtrip(
|
|||
eprintln!("[roundtrip] Generated {} bytes", result.output_nickel.len());
|
||||
}
|
||||
|
||||
// Create and print terminal summary
|
||||
use typedialog_core::nickel::summary::RoundtripSummary;
|
||||
|
||||
let summary = RoundtripSummary::from_values(
|
||||
&result.initial_values,
|
||||
&result.form_results,
|
||||
result.validation_passed,
|
||||
result.output_path.display().to_string(),
|
||||
);
|
||||
|
||||
// Print terminal summary
|
||||
print!("{}", summary.render_terminal(verbose));
|
||||
|
||||
// Print summary
|
||||
println!("✓ Roundtrip completed successfully (TUI backend)");
|
||||
println!(" Input fields: {}", result.form_results.len());
|
||||
println!(
|
||||
"\n✅ Configuration saved to: {}\n",
|
||||
result.output_path.display()
|
||||
" Imports preserved: {}",
|
||||
result.input_contracts.imports.len()
|
||||
);
|
||||
println!("Next steps:");
|
||||
println!(
|
||||
" - Review the configuration: cat {}",
|
||||
result.output_path.display()
|
||||
" Contracts preserved: {}",
|
||||
result.input_contracts.field_contracts.len()
|
||||
);
|
||||
println!(" - Apply CI tools: (run your CI setup command)");
|
||||
println!(" - Re-run this script anytime to update your configuration\n");
|
||||
|
||||
if let Some(passed) = result.validation_passed {
|
||||
if !passed {
|
||||
if passed {
|
||||
println!(" ✓ Validation: PASSED");
|
||||
} else {
|
||||
println!(" ✗ Validation: FAILED");
|
||||
return Err(Error::validation_failed(
|
||||
"Nickel typecheck failed on output",
|
||||
));
|
||||
|
|
|
|||
|
|
@ -149,14 +149,6 @@ enum Commands {
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize tracing subscriber with env filter (respects RUST_LOG)
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
// Load configuration with CLI override
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ tokio = { workspace = true, features = ["rt-multi-thread"] }
|
|||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
unic-langid = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -211,14 +211,6 @@ fn extract_nickel_defaults(
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize tracing subscriber with env filter (respects RUST_LOG)
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
// Load configuration with CLI override
|
||||
|
|
@ -320,7 +312,7 @@ fn load_nickel_defaults(
|
|||
let form_fields: Vec<form_parser::FieldDefinition> = form_elements
|
||||
.iter()
|
||||
.filter_map(|elem| match elem {
|
||||
form_parser::FormElement::Field(f) => Some(f.as_ref().clone()),
|
||||
form_parser::FormElement::Field(f) => Some(f.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -401,7 +393,7 @@ async fn execute_form(
|
|||
.elements
|
||||
.iter()
|
||||
.filter_map(|elem| match elem {
|
||||
form_parser::FormElement::Field(f) => Some(f.as_ref().clone()),
|
||||
form_parser::FormElement::Field(f) => Some(f.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -435,7 +427,7 @@ async fn execute_form(
|
|||
.elements
|
||||
.iter()
|
||||
.filter_map(|elem| match elem {
|
||||
form_parser::FormElement::Field(f) => Some(f.as_ref().clone()),
|
||||
form_parser::FormElement::Field(f) => Some(f.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -597,32 +589,11 @@ async fn nickel_roundtrip_cmd(
|
|||
let port = 8080;
|
||||
let mut backend = BackendFactory::create(BackendType::Web { port })?;
|
||||
|
||||
// Set roundtrip context for web backend so it can show summary page
|
||||
if let Some(init_vals) = &initial_values {
|
||||
use typedialog_core::backends::web::RoundtripContext;
|
||||
|
||||
// Downcast to WebBackend to access state
|
||||
let web_backend = backend
|
||||
.as_any()
|
||||
.downcast_ref::<typedialog_core::backends::web::WebBackend>()
|
||||
.ok_or_else(|| Error::validation_failed("Expected WebBackend"))?;
|
||||
|
||||
if let Some(state) = web_backend.get_state() {
|
||||
let context = RoundtripContext {
|
||||
initial_values: init_vals.clone(),
|
||||
output_path: output.clone(),
|
||||
input_nickel: input_source.clone(),
|
||||
};
|
||||
state.set_roundtrip_context(context).await;
|
||||
}
|
||||
}
|
||||
|
||||
println!("Starting interactive form on http://localhost:{}", port);
|
||||
println!("Complete the form and submit to continue...\n");
|
||||
|
||||
// USE THE SAME EXECUTION PATH AS NORMAL WEB BACKEND
|
||||
// This respects display_mode and calls execute_form_complete() when display_mode = "complete"
|
||||
let initial_values_backup = initial_values.clone();
|
||||
let form_results = form_parser::execute_with_backend_i18n_with_defaults(
|
||||
form,
|
||||
backend.as_mut(),
|
||||
|
|
@ -698,29 +669,24 @@ async fn nickel_roundtrip_cmd(
|
|||
true
|
||||
};
|
||||
|
||||
// Create and print terminal summary
|
||||
use typedialog_core::nickel::summary::RoundtripSummary;
|
||||
|
||||
let summary = RoundtripSummary::from_values(
|
||||
&initial_values_backup.unwrap_or_default(),
|
||||
&form_results,
|
||||
Some(validation_passed),
|
||||
output.display().to_string(),
|
||||
// Print summary
|
||||
println!("✓ Roundtrip completed successfully (Web backend - interactive)");
|
||||
println!(" Input fields: {}", form_results.len());
|
||||
println!(" Imports preserved: {}", input_contracts.imports.len());
|
||||
println!(
|
||||
" Contracts preserved: {}",
|
||||
input_contracts.field_contracts.len()
|
||||
);
|
||||
|
||||
// Print terminal summary
|
||||
print!("{}", summary.render_terminal(false));
|
||||
|
||||
println!("\n✅ Configuration saved to: {}\n", output.display());
|
||||
println!("Next steps:");
|
||||
println!(" - Review the configuration: cat {}", output.display());
|
||||
println!(" - Apply CI tools: (run your CI setup command)");
|
||||
println!(" - Re-run this script anytime to update: .typedialog/ci/ci-configure.sh\n");
|
||||
|
||||
if !validation_passed {
|
||||
return Err(Error::validation_failed(
|
||||
"Nickel typecheck failed on output",
|
||||
));
|
||||
if validate {
|
||||
if validation_passed {
|
||||
println!(" ✓ Validation: PASSED");
|
||||
} else {
|
||||
println!(" ✗ Validation: FAILED");
|
||||
return Err(Error::validation_failed(
|
||||
"Nickel typecheck failed on output",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ tokio = { workspace = true, features = ["rt-multi-thread"] }
|
|||
serde_json = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
unic-langid = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -175,33 +175,22 @@ pub fn nickel_roundtrip(
|
|||
eprintln!("[roundtrip] Generated {} bytes", result.output_nickel.len());
|
||||
}
|
||||
|
||||
// Create and print terminal summary
|
||||
use typedialog_core::nickel::summary::RoundtripSummary;
|
||||
|
||||
let summary = RoundtripSummary::from_values(
|
||||
&result.initial_values,
|
||||
&result.form_results,
|
||||
result.validation_passed,
|
||||
result.output_path.display().to_string(),
|
||||
);
|
||||
|
||||
// Print terminal summary
|
||||
print!("{}", summary.render_terminal(verbose));
|
||||
|
||||
println!("✓ Roundtrip completed successfully");
|
||||
println!(" Input fields: {}", result.form_results.len());
|
||||
println!(
|
||||
"\n✅ Configuration saved to: {}\n",
|
||||
result.output_path.display()
|
||||
" Imports preserved: {}",
|
||||
result.input_contracts.imports.len()
|
||||
);
|
||||
println!("Next steps:");
|
||||
println!(
|
||||
" - Review the configuration: cat {}",
|
||||
result.output_path.display()
|
||||
" Contracts preserved: {}",
|
||||
result.input_contracts.field_contracts.len()
|
||||
);
|
||||
println!(" - Apply CI tools: (run your CI setup command)");
|
||||
println!(" - Re-run this script anytime to update your configuration\n");
|
||||
|
||||
if let Some(passed) = result.validation_passed {
|
||||
if !passed {
|
||||
if passed {
|
||||
println!(" ✓ Validation: PASSED");
|
||||
} else {
|
||||
println!(" ✗ Validation: FAILED");
|
||||
return Err(Error::validation_failed(
|
||||
"Nickel typecheck failed on output",
|
||||
));
|
||||
|
|
|
|||
|
|
@ -300,14 +300,6 @@ enum Commands {
|
|||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize tracing subscriber with env filter (respects RUST_LOG)
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
|
||||
)
|
||||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
// Load configuration with CLI override
|
||||
|
|
|
|||
|
|
@ -453,135 +453,12 @@ when = "database_driver == mysql"
|
|||
required = true
|
||||
```
|
||||
|
||||
### Comparison Operators
|
||||
**Supported operators**:
|
||||
|
||||
**Equality and inequality**:
|
||||
|
||||
- `==`: Equal to
|
||||
- `!=`: Not equal to
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "postgres_config"
|
||||
type = "text"
|
||||
when = "database_driver == postgresql"
|
||||
|
||||
[[elements]]
|
||||
name = "legacy_warning"
|
||||
type = "section"
|
||||
content = "⚠️ SQLite is for development only"
|
||||
when = "database_driver != postgresql"
|
||||
```
|
||||
|
||||
**Numeric comparisons**:
|
||||
|
||||
- `>`: Greater than
|
||||
- `<`: Less than
|
||||
- `>=`: Greater than or equal
|
||||
- `<=`: Less than or equal
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "performance_warning"
|
||||
type = "section"
|
||||
content = "⚠️ High port number may require elevated privileges"
|
||||
when = "port >= 1024"
|
||||
|
||||
[[elements]]
|
||||
name = "pool_size_warning"
|
||||
type = "section"
|
||||
when = "connection_pool > 100"
|
||||
```
|
||||
|
||||
### String Operators
|
||||
|
||||
**String matching**:
|
||||
|
||||
- `contains`: Check if field contains substring
|
||||
- `startswith`: Check if field starts with prefix
|
||||
- `endswith`: Check if field ends with suffix
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "rust_specific"
|
||||
type = "text"
|
||||
prompt = "Rust toolchain version"
|
||||
when = "language contains rust"
|
||||
|
||||
[[elements]]
|
||||
name = "protocol_warning"
|
||||
type = "section"
|
||||
when = "url startswith https"
|
||||
|
||||
[[elements]]
|
||||
name = "yaml_parser"
|
||||
type = "select"
|
||||
when = "config_file endswith .yaml"
|
||||
```
|
||||
|
||||
### Array Membership
|
||||
|
||||
**`in` operator**: Check if value exists in array field
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "rust_features"
|
||||
type = "multiselect"
|
||||
prompt = "Rust-specific features"
|
||||
when = "rust in detected_languages"
|
||||
options = [
|
||||
{ value = "clippy", label = "Clippy linting" },
|
||||
{ value = "cargo_audit", label = "Security auditing" }
|
||||
]
|
||||
|
||||
[[elements]]
|
||||
name = "python_virtualenv"
|
||||
type = "confirm"
|
||||
when = "python in languages"
|
||||
```
|
||||
|
||||
**Note**: The `in` operator works with:
|
||||
|
||||
- Array fields (JSON array values)
|
||||
- MultiSelect fields (comma-separated strings)
|
||||
|
||||
### File System Conditions
|
||||
|
||||
**`file_exists(path)`**: Check if file or directory exists
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "use_existing_config"
|
||||
type = "confirm"
|
||||
prompt = "Existing config.toml found. Use it?"
|
||||
when = "file_exists(config.toml)"
|
||||
|
||||
[[elements]]
|
||||
name = "create_new_config"
|
||||
type = "text"
|
||||
prompt = "Configuration name"
|
||||
when = "!file_exists(.env)"
|
||||
```
|
||||
|
||||
**Negation with `!`**:
|
||||
|
||||
- `!file_exists(path)`: File does NOT exist
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "docker_setup"
|
||||
type = "group"
|
||||
includes = ["fragments/docker-init.toml"]
|
||||
when = "!file_exists(Dockerfile)"
|
||||
```
|
||||
|
||||
### Future Support
|
||||
|
||||
**Planned features**:
|
||||
|
||||
- Parentheses for grouping: `(a == b) && (c == d)`
|
||||
- Logical AND: `&&`
|
||||
- Logical OR: `||`
|
||||
- `==`: Equality
|
||||
- `!=`: Inequality
|
||||
- Parentheses for grouping (future)
|
||||
- Logical AND/OR (future)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
332
docs/nickel.md
332
docs/nickel.md
|
|
@ -104,22 +104,15 @@ let rendered = template_engine.render("form.j2", &context)?;
|
|||
|
||||
### roundtrip
|
||||
|
||||
Idempotent read/write for Nickel schemas - complete workflow from `.ncl` → form → `.ncl`.
|
||||
Idempotent read/write for Nickel schemas.
|
||||
|
||||
```rust
|
||||
use typedialog_core::nickel::RoundtripConfig;
|
||||
|
||||
let mut config = RoundtripConfig::with_template(
|
||||
input_ncl, // Path to existing .ncl file
|
||||
form_toml, // Path to form definition
|
||||
output_ncl, // Path for generated .ncl
|
||||
template, // Optional .ncl.j2 template
|
||||
);
|
||||
config.validate = true;
|
||||
config.verbose = true;
|
||||
|
||||
// Execute with any backend (CLI, TUI, Web)
|
||||
let result = config.execute_with_backend(backend.as_mut()).await?;
|
||||
let config = RoundtripConfig::default();
|
||||
let schema = parser.parse_file("form.ncl")?;
|
||||
let modified = transform(schema)?;
|
||||
serializer.write_file("form.ncl", &modified, &config)?;
|
||||
```
|
||||
|
||||
**Preserves:**
|
||||
|
|
@ -128,13 +121,6 @@ let result = config.execute_with_backend(backend.as_mut()).await?;
|
|||
- Formatting
|
||||
- Import structure
|
||||
- Custom layouts
|
||||
- Field contracts and validators
|
||||
|
||||
**Returns:**
|
||||
|
||||
- `RoundtripSummary` with diff viewer
|
||||
- Validation status
|
||||
- Change detection (what fields changed)
|
||||
|
||||
## Schema Structure
|
||||
|
||||
|
|
@ -202,314 +188,6 @@ let result = config.execute_with_backend(backend.as_mut()).await?;
|
|||
}
|
||||
```
|
||||
|
||||
## Roundtrip Workflow
|
||||
|
||||
The roundtrip workflow enables interactive reconfiguration of existing Nickel files through forms, preserving structure and generating human-readable diffs.
|
||||
|
||||
### Roundtrip Overview
|
||||
|
||||
**Workflow:** `config.ncl` (input) → **Form** (edit) → `config.ncl` (output)
|
||||
|
||||
1. **Load** existing `.ncl` configuration
|
||||
2. **Extract** default values using `nickel_path` mappings
|
||||
3. **Populate** form with current values
|
||||
4. **Edit** via CLI/TUI/Web interface
|
||||
5. **Generate** new `.ncl` using template (or preserve contracts)
|
||||
6. **Validate** with `nickel typecheck`
|
||||
7. **Show** summary with diff viewer
|
||||
|
||||
### The `nickel_path` Attribute
|
||||
|
||||
**Critical:** Fields MUST have `nickel_path` to participate in roundtrip.
|
||||
|
||||
Maps form field names to nested Nickel structure:
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "parallel_jobs"
|
||||
type = "text"
|
||||
prompt = "Parallel Jobs"
|
||||
default = "4"
|
||||
nickel_path = ["ci", "github_actions", "parallel_jobs"]
|
||||
```
|
||||
|
||||
Extracts from:
|
||||
|
||||
```nickel
|
||||
{
|
||||
ci = {
|
||||
github_actions = {
|
||||
parallel_jobs = 4
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
|
||||
- Array of strings representing path
|
||||
- Each element is a nested key
|
||||
- Top-level: `["field_name"]`
|
||||
- Nested: `["parent", "child", "field"]`
|
||||
- Arrays: Use RepeatingGroup with `nickel_path = ["array_name"]`
|
||||
|
||||
### Roundtrip with CLI Backend
|
||||
|
||||
```bash
|
||||
typedialog nickel-roundtrip \
|
||||
--input config.ncl \
|
||||
--form ci-form.toml \
|
||||
--output config.ncl \
|
||||
--ncl-template config.ncl.j2 \
|
||||
--verbose
|
||||
```
|
||||
|
||||
**Output:**
|
||||
|
||||
```text
|
||||
╔════════════════════════════════════════════════════════════╗
|
||||
║ ✅ Configuration Saved Successfully! ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
║ 📄 File: config.ncl ║
|
||||
║ ✓ Validation: ✓ PASSED ║
|
||||
║ 📊 Fields: 5/27 changed, 22 unchanged ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
║ 📋 What Changed: ║
|
||||
║ ├─ parallel_jobs: 4 → 8 ║
|
||||
║ ├─ timeout_minutes: 60 → 120 ║
|
||||
║ ├─ enable_clippy: true → false ║
|
||||
║ ├─ rust_version: stable → nightly ║
|
||||
║ ├─ cache_enabled: false → true ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
║ 💡 Next Steps: ║
|
||||
║ • Review: cat config.ncl ║
|
||||
║ • Apply CI tools: ./setup-ci.sh ║
|
||||
║ • Re-configure: ./ci-configure.sh ║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
### Roundtrip with TUI Backend
|
||||
|
||||
```bash
|
||||
typedialog-tui nickel-roundtrip \
|
||||
--input config.ncl \
|
||||
--form ci-form.toml \
|
||||
--output config.ncl \
|
||||
--ncl-template config.ncl.j2
|
||||
```
|
||||
|
||||
Interactive TUI form + terminal summary (same as CLI).
|
||||
|
||||
### Roundtrip with Web Backend
|
||||
|
||||
```bash
|
||||
typedialog-web nickel-roundtrip \
|
||||
--input config.ncl \
|
||||
--form ci-form.toml \
|
||||
--output config.ncl \
|
||||
--ncl-template config.ncl.j2 \
|
||||
--verbose
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- Opens browser to `http://localhost:8080`
|
||||
- All fields pre-populated with current values
|
||||
- Real-time validation
|
||||
- **Summary page** on submit:
|
||||
- Visual diff viewer (old → new)
|
||||
- Field statistics
|
||||
- Download button for generated config
|
||||
- Auto-close after 30 seconds
|
||||
|
||||
**Terminal output:**
|
||||
|
||||
```text
|
||||
Starting interactive form on http://localhost:8080
|
||||
Complete the form and submit to continue...
|
||||
|
||||
Web UI available at http://localhost:8080
|
||||
[web] Complete form initialized with 63 default values
|
||||
|
||||
╔════════════════════════════════════════════════════════════╗
|
||||
║ ✅ Configuration Saved Successfully! ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
...
|
||||
```
|
||||
|
||||
### Form Requirements
|
||||
|
||||
For roundtrip to work, your form MUST:
|
||||
|
||||
1. **Include `nickel_path` on ALL fields:**
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "project_name"
|
||||
nickel_path = ["project", "name"] # ✅ Required
|
||||
```
|
||||
|
||||
2. **Use correct path syntax:**
|
||||
|
||||
```toml
|
||||
# Flat field
|
||||
nickel_path = ["enable_ci"]
|
||||
|
||||
# Nested field
|
||||
nickel_path = ["ci", "github_actions", "timeout"]
|
||||
|
||||
# Array field (RepeatingGroup)
|
||||
nickel_path = ["ci", "tools", "linters"]
|
||||
```
|
||||
|
||||
3. **Match template variables:**
|
||||
|
||||
```jinja2
|
||||
# Template: config.ncl.j2
|
||||
{
|
||||
project = {
|
||||
name = "{{ project_name }}", # ← Form field "project_name"
|
||||
},
|
||||
ci = {
|
||||
enabled = {{ enable_ci }}, # ← Form field "enable_ci"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Template Support
|
||||
|
||||
Use Tera templates (`.ncl.j2`) for complex output structures:
|
||||
|
||||
**Template:** `config.ncl.j2`
|
||||
|
||||
```jinja2
|
||||
# Generated by TypeDialog
|
||||
let imports = import "ci/lib.ncl"
|
||||
let validators = import "ci/validators.ncl"
|
||||
|
||||
{
|
||||
project = {
|
||||
name = "{{ project_name }}",
|
||||
version = "{{ project_version }}",
|
||||
},
|
||||
|
||||
ci = {
|
||||
github_actions = {
|
||||
enabled = {{ enable_github_actions }},
|
||||
parallel_jobs = {{ parallel_jobs }},
|
||||
timeout_minutes = {{ timeout_minutes }},
|
||||
|
||||
{% if enable_cache %}
|
||||
cache = {
|
||||
enabled = true,
|
||||
paths = {{ cache_paths | json }},
|
||||
},
|
||||
{% endif %}
|
||||
},
|
||||
|
||||
tools = {
|
||||
{% for tool in ci_tools %}
|
||||
{{ tool.name }} = {
|
||||
enabled = {{ tool.enabled }},
|
||||
version = "{{ tool.version }}",
|
||||
},
|
||||
{% endfor %}
|
||||
},
|
||||
}
|
||||
} | validators.CiConfig
|
||||
```
|
||||
|
||||
**Form values are injected automatically.**
|
||||
|
||||
### Complete Example
|
||||
|
||||
See `examples/08-nickel-roundtrip/` for a full CI configuration workflow:
|
||||
|
||||
```bash
|
||||
cd examples/08-nickel-roundtrip/
|
||||
|
||||
# Initial setup
|
||||
./01-generate-initial-config.sh
|
||||
|
||||
# Edit with CLI
|
||||
./02-roundtrip-cli.sh
|
||||
|
||||
# Edit with TUI
|
||||
./03-roundtrip-tui.sh
|
||||
|
||||
# Edit with Web
|
||||
./04-roundtrip-web.sh
|
||||
```
|
||||
|
||||
### Validation
|
||||
|
||||
Roundtrip automatically validates output:
|
||||
|
||||
```bash
|
||||
typedialog nickel-roundtrip \
|
||||
--input config.ncl \
|
||||
--form form.toml \
|
||||
--output config.ncl \
|
||||
--ncl-template template.ncl.j2
|
||||
# Runs: nickel typecheck config.ncl
|
||||
```
|
||||
|
||||
**Disable validation:**
|
||||
|
||||
```bash
|
||||
typedialog nickel-roundtrip ... --no-validate
|
||||
```
|
||||
|
||||
### Summary Output
|
||||
|
||||
All backends generate summaries showing:
|
||||
|
||||
- **Total fields:** How many fields in the form
|
||||
- **Changed fields:** Fields with different values
|
||||
- **Unchanged fields:** Fields that kept the same value
|
||||
- **Validation status:** Pass/fail from `nickel typecheck`
|
||||
- **Change list:** Detailed old → new for each change
|
||||
|
||||
**Verbose mode** shows ALL changes (not just first 10):
|
||||
|
||||
```bash
|
||||
typedialog nickel-roundtrip ... --verbose
|
||||
```
|
||||
|
||||
### Roundtrip Troubleshooting
|
||||
|
||||
**Issue: "No default values loaded"**
|
||||
|
||||
✓ Check all fields have `nickel_path`:
|
||||
|
||||
```bash
|
||||
grep -r "nickel_path" form.toml
|
||||
```
|
||||
|
||||
**Issue: "Field not found in output"**
|
||||
|
||||
✓ Verify template includes the field:
|
||||
|
||||
```bash
|
||||
grep "{{ field_name }}" template.ncl.j2
|
||||
```
|
||||
|
||||
**Issue: "Validation failed"**
|
||||
|
||||
✓ Check Nickel syntax manually:
|
||||
|
||||
```bash
|
||||
nickel typecheck config.ncl
|
||||
```
|
||||
|
||||
**Issue: "Values not showing in web form"**
|
||||
|
||||
✓ Ensure `nickel export` works on input:
|
||||
|
||||
```bash
|
||||
nickel export config.ncl
|
||||
```
|
||||
|
||||
## Building with Nickel
|
||||
|
||||
Build project with Nickel support:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Nickel Integration
|
||||
|
||||
Type-safe form schema generation and roundtrip workflows using Nickel configuration language.
|
||||
Type-safe form schema generation using Nickel configuration language.
|
||||
|
||||
## Files
|
||||
|
||||
|
|
@ -10,12 +10,10 @@ Type-safe form schema generation and roundtrip workflows using Nickel configurat
|
|||
## About Nickel
|
||||
|
||||
Nickel is a powerful configuration language that provides:
|
||||
|
||||
- Strong typing
|
||||
- Validation rules
|
||||
- Reusable schemas
|
||||
- Inheritance and composition
|
||||
- **Roundtrip support** - Edit existing configs via forms
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -32,44 +30,6 @@ nickel eval nickel_schema.ncl > form_config.toml
|
|||
cargo run -p typedialog-web -- --config form_config.toml
|
||||
```
|
||||
|
||||
### Roundtrip Workflow (Edit Existing Configs)
|
||||
|
||||
**New!** Edit existing Nickel configurations through interactive forms:
|
||||
|
||||
```bash
|
||||
# CLI backend (command-line prompts)
|
||||
typedialog nickel-roundtrip \
|
||||
--input config.ncl \
|
||||
--form ci-form.toml \
|
||||
--output config.ncl \
|
||||
--ncl-template config.ncl.j2
|
||||
|
||||
# TUI backend (full-screen terminal UI)
|
||||
typedialog-tui nickel-roundtrip \
|
||||
--input config.ncl \
|
||||
--form ci-form.toml \
|
||||
--output config.ncl \
|
||||
--ncl-template config.ncl.j2
|
||||
|
||||
# Web backend (browser-based form with HTML diff)
|
||||
typedialog-web nickel-roundtrip \
|
||||
--input config.ncl \
|
||||
--form ci-form.toml \
|
||||
--output config.ncl \
|
||||
--ncl-template config.ncl.j2
|
||||
```
|
||||
|
||||
**Features:**
|
||||
|
||||
- ✓ Load existing values from `.ncl` files
|
||||
- ✓ Pre-populate form fields with current config
|
||||
- ✓ Generate new `.ncl` using templates
|
||||
- ✓ Show diff summary (what changed)
|
||||
- ✓ Automatic validation with `nickel typecheck`
|
||||
- ✓ HTML summary page (web backend only)
|
||||
|
||||
**See complete example:** `../08-nickel-roundtrip/`
|
||||
|
||||
### Example Nickel Schema
|
||||
|
||||
```nickel
|
||||
|
|
@ -97,13 +57,11 @@ typedialog-web nickel-roundtrip \
|
|||
|
||||
## Advanced Features
|
||||
|
||||
- **Field inheritance** - Reuse common field definitions
|
||||
- **Custom validators** - Built-in schema validation
|
||||
- **Conditional schemas** - Dynamic form generation
|
||||
- **Template-driven generation** - Tera template support
|
||||
- **Schema composition** - Combine multiple schemas
|
||||
- **Roundtrip editing** - Edit existing configs via forms (NEW!)
|
||||
- **Diff viewer** - See what changed after editing (NEW!)
|
||||
- Field inheritance
|
||||
- Custom validators
|
||||
- Conditional schemas
|
||||
- Template-driven generation
|
||||
- Schema composition
|
||||
|
||||
## Learn More
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "📝 Generating initial CI configuration..."
|
||||
echo ""
|
||||
|
||||
# config.ncl is already provided as the initial state
|
||||
if [ -f "config.ncl" ]; then
|
||||
echo "✅ Initial config.ncl already exists"
|
||||
echo ""
|
||||
echo "Current configuration:"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
cat config.ncl
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " ./02-roundtrip-cli.sh # Edit with CLI backend"
|
||||
echo " ./03-roundtrip-tui.sh # Edit with TUI backend"
|
||||
echo " ./04-roundtrip-web.sh # Edit with Web backend"
|
||||
else
|
||||
echo "❌ config.ncl not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "🔧 Running nickel-roundtrip with CLI backend..."
|
||||
echo ""
|
||||
|
||||
# Check if config.ncl exists
|
||||
if [ ! -f "config.ncl" ]; then
|
||||
echo "❌ config.ncl not found! Run ./01-generate-initial-config.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Backup current config
|
||||
cp config.ncl "config.ncl.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
# Run roundtrip with CLI backend
|
||||
cargo run -p typedialog --release -- nickel-roundtrip \
|
||||
--input config.ncl \
|
||||
--form ci-form.toml \
|
||||
--output config.ncl \
|
||||
--ncl-template config.ncl.j2 \
|
||||
--verbose
|
||||
|
||||
echo ""
|
||||
echo "✅ Roundtrip completed!"
|
||||
echo ""
|
||||
echo "Review changes:"
|
||||
echo " cat config.ncl"
|
||||
echo ""
|
||||
echo "Run again:"
|
||||
echo " ./02-roundtrip-cli.sh"
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "🎨 Running nickel-roundtrip with TUI backend..."
|
||||
echo ""
|
||||
|
||||
# Check if config.ncl exists
|
||||
if [ ! -f "config.ncl" ]; then
|
||||
echo "❌ config.ncl not found! Run ./01-generate-initial-config.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Backup current config
|
||||
cp config.ncl "config.ncl.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
# Run roundtrip with TUI backend
|
||||
cargo run -p typedialog-tui --release -- nickel-roundtrip \
|
||||
--input config.ncl \
|
||||
--form ci-form.toml \
|
||||
--output config.ncl \
|
||||
--ncl-template config.ncl.j2 \
|
||||
--verbose
|
||||
|
||||
echo ""
|
||||
echo "✅ Roundtrip completed!"
|
||||
echo ""
|
||||
echo "Review changes:"
|
||||
echo " cat config.ncl"
|
||||
echo ""
|
||||
echo "Run again:"
|
||||
echo " ./03-roundtrip-tui.sh"
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "🌐 Running nickel-roundtrip with Web backend..."
|
||||
echo ""
|
||||
|
||||
# Check if config.ncl exists
|
||||
if [ ! -f "config.ncl" ]; then
|
||||
echo "❌ config.ncl not found! Run ./01-generate-initial-config.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Backup current config
|
||||
cp config.ncl "config.ncl.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
|
||||
echo "Starting web server on http://localhost:8080"
|
||||
echo "Your browser should open automatically..."
|
||||
echo ""
|
||||
echo "Features:"
|
||||
echo " ✓ All fields pre-populated with current values"
|
||||
echo " ✓ Real-time validation"
|
||||
echo " ✓ HTML diff viewer on submit"
|
||||
echo " ✓ Download button for config.ncl"
|
||||
echo " ✓ Auto-close after 30 seconds"
|
||||
echo ""
|
||||
echo "Press Ctrl+C after form is submitted to exit"
|
||||
echo ""
|
||||
|
||||
# Open browser (macOS)
|
||||
sleep 2 && open "http://localhost:8080" &
|
||||
|
||||
# Run roundtrip with Web backend
|
||||
cargo run -p typedialog-web --release -- nickel-roundtrip \
|
||||
--input config.ncl \
|
||||
--form ci-form.toml \
|
||||
--output config.ncl \
|
||||
--ncl-template config.ncl.j2 \
|
||||
--verbose
|
||||
|
||||
echo ""
|
||||
echo "✅ Roundtrip completed!"
|
||||
echo ""
|
||||
echo "Review changes:"
|
||||
echo " cat config.ncl"
|
||||
echo ""
|
||||
echo "Run again:"
|
||||
echo " ./04-roundtrip-web.sh"
|
||||
|
|
@ -1,260 +0,0 @@
|
|||
# Nickel Roundtrip Example
|
||||
|
||||
Complete example demonstrating the roundtrip workflow: load existing configuration, edit via interactive form, and regenerate with diff viewer.
|
||||
|
||||
## Scenario
|
||||
|
||||
CI configuration management - edit GitHub Actions settings through a form interface while preserving the Nickel structure.
|
||||
|
||||
## Files
|
||||
|
||||
- **config.ncl** - Input/output Nickel configuration
|
||||
- **ci-form.toml** - Form definition with `nickel_path` mappings
|
||||
- **config.ncl.j2** - Tera template for generating output
|
||||
- **01-generate-initial-config.sh** - Create initial config
|
||||
- **02-roundtrip-cli.sh** - Edit via CLI backend
|
||||
- **03-roundtrip-tui.sh** - Edit via TUI backend
|
||||
- **04-roundtrip-web.sh** - Edit via Web backend
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Generate initial configuration
|
||||
./01-generate-initial-config.sh
|
||||
|
||||
# 2. Edit with your preferred backend:
|
||||
|
||||
# CLI (command-line prompts)
|
||||
./02-roundtrip-cli.sh
|
||||
|
||||
# TUI (full-screen terminal UI)
|
||||
./03-roundtrip-tui.sh
|
||||
|
||||
# Web (browser-based form)
|
||||
./04-roundtrip-web.sh
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### `nickel_path` Mapping
|
||||
|
||||
Every field in `ci-form.toml` has a `nickel_path` attribute mapping to the Nickel structure:
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "parallel_jobs"
|
||||
type = "text"
|
||||
prompt = "Parallel Jobs"
|
||||
default = "4"
|
||||
nickel_path = ["ci", "github_actions", "parallel_jobs"]
|
||||
```
|
||||
|
||||
Maps to:
|
||||
|
||||
```nickel
|
||||
{
|
||||
ci = {
|
||||
github_actions = {
|
||||
parallel_jobs = 4
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Template Rendering
|
||||
|
||||
The `config.ncl.j2` template uses form values to generate valid Nickel:
|
||||
|
||||
```jinja2
|
||||
{
|
||||
project = {
|
||||
name = "{{ project_name }}",
|
||||
description = "{{ project_description }}",
|
||||
},
|
||||
|
||||
ci = {
|
||||
github_actions = {
|
||||
enabled = {{ enable_github_actions }},
|
||||
parallel_jobs = {{ parallel_jobs }},
|
||||
timeout_minutes = {{ timeout_minutes }},
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Summary with Diff
|
||||
|
||||
After editing, see what changed:
|
||||
|
||||
```text
|
||||
╔════════════════════════════════════════════════════════════╗
|
||||
║ ✅ Configuration Saved Successfully! ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
║ 📄 File: config.ncl ║
|
||||
║ ✓ Validation: ✓ PASSED ║
|
||||
║ 📊 Fields: 3/12 changed, 9 unchanged ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
║ 📋 What Changed: ║
|
||||
║ ├─ parallel_jobs: 4 → 8 ║
|
||||
║ ├─ timeout_minutes: 60 → 120 ║
|
||||
║ ├─ enable_cache: false → true ║
|
||||
╠════════════════════════════════════════════════════════════╣
|
||||
║ 💡 Next Steps: ║
|
||||
║ • Review: cat config.ncl ║
|
||||
║ • Apply CI tools: ./setup-ci.sh ║
|
||||
║ • Re-configure: ./ci-configure.sh ║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
## Workflow Details
|
||||
|
||||
### 1. Load Defaults
|
||||
|
||||
Roundtrip reads `config.ncl` and extracts values using `nickel_path`:
|
||||
|
||||
```rust
|
||||
// Internally:
|
||||
nickel export config.ncl // → JSON
|
||||
extract_value_by_path(json, ["ci", "github_actions", "parallel_jobs"]) // → 4
|
||||
```
|
||||
|
||||
### 2. Populate Form
|
||||
|
||||
Form fields get pre-filled with current values:
|
||||
|
||||
- Text fields show current text
|
||||
- Numbers show current numbers
|
||||
- Booleans show current true/false
|
||||
- Select fields pre-select current option
|
||||
|
||||
### 3. Edit Interactively
|
||||
|
||||
User edits via chosen backend (CLI/TUI/Web).
|
||||
|
||||
### 4. Generate Output
|
||||
|
||||
Template renders with new values:
|
||||
|
||||
```jinja2
|
||||
parallel_jobs = {{ parallel_jobs }}, // User changed 4 → 8
|
||||
```
|
||||
|
||||
### 5. Validate
|
||||
|
||||
Automatic validation:
|
||||
|
||||
```bash
|
||||
nickel typecheck config.ncl
|
||||
```
|
||||
|
||||
### 6. Show Summary
|
||||
|
||||
Terminal summary (all backends) + HTML summary (web only).
|
||||
|
||||
## Backend Comparison
|
||||
|
||||
| Feature | CLI | TUI | Web |
|
||||
|---------|-----|-----|-----|
|
||||
| Terminal UI | ✓ | ✓ | ✓ (summary only) |
|
||||
| Browser UI | ✗ | ✗ | ✓ |
|
||||
| Pre-populated values | ✓ | ✓ | ✓ |
|
||||
| Real-time validation | ✗ | ✓ | ✓ |
|
||||
| HTML diff viewer | ✗ | ✗ | ✓ |
|
||||
| Download button | ✗ | ✗ | ✓ |
|
||||
|
||||
## Customization
|
||||
|
||||
### Add More Fields
|
||||
|
||||
1. **Update form:**
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "rust_version"
|
||||
type = "select"
|
||||
prompt = "Rust Version"
|
||||
options = [
|
||||
{ value = "stable", label = "Stable" },
|
||||
{ value = "nightly", label = "Nightly" }
|
||||
]
|
||||
nickel_path = ["ci", "rust", "version"]
|
||||
```
|
||||
|
||||
2. **Update template:**
|
||||
|
||||
```jinja2
|
||||
ci = {
|
||||
rust = {
|
||||
version = "{{ rust_version }}",
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update initial config:**
|
||||
|
||||
```nickel
|
||||
{
|
||||
ci = {
|
||||
rust = {
|
||||
version = "stable"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Change Template Logic
|
||||
|
||||
Add conditionals:
|
||||
|
||||
```jinja2
|
||||
{% if enable_cache %}
|
||||
cache = {
|
||||
enabled = true,
|
||||
paths = {{ cache_paths | json }},
|
||||
},
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
### Disable Validation
|
||||
|
||||
```bash
|
||||
typedialog nickel-roundtrip \
|
||||
--input config.ncl \
|
||||
--form ci-form.toml \
|
||||
--output config.ncl \
|
||||
--ncl-template config.ncl.j2 \
|
||||
--no-validate # ← Skip nickel typecheck
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**No defaults loaded:**
|
||||
|
||||
```bash
|
||||
# Check nickel export works
|
||||
nickel export config.ncl
|
||||
|
||||
# Verify all fields have nickel_path
|
||||
grep "nickel_path" ci-form.toml | wc -l
|
||||
```
|
||||
|
||||
**Template errors:**
|
||||
|
||||
```bash
|
||||
# Test template separately
|
||||
echo '{"project_name": "test"}' | \
|
||||
tera --template config.ncl.j2
|
||||
```
|
||||
|
||||
**Validation fails:**
|
||||
|
||||
```bash
|
||||
# Check manually
|
||||
nickel typecheck config.ncl
|
||||
```
|
||||
|
||||
## Learn More
|
||||
|
||||
- [nickel.md](../../docs/nickel.md) - Full roundtrip documentation
|
||||
- [Nickel Language](https://nickel-lang.org) - Nickel reference
|
||||
- [Tera Templates](https://tera.netlify.app) - Template syntax
|
||||
|
|
@ -1,237 +0,0 @@
|
|||
name = "CI Configuration Editor"
|
||||
description = "Interactive form for editing CI/CD configuration"
|
||||
display_mode = "complete"
|
||||
|
||||
# =============================================================================
|
||||
# PROJECT INFORMATION
|
||||
# =============================================================================
|
||||
|
||||
[[elements]]
|
||||
name = "project_header"
|
||||
type = "section_header"
|
||||
title = "📦 Project Information"
|
||||
border_top = true
|
||||
border_bottom = true
|
||||
|
||||
[[elements]]
|
||||
name = "project_name"
|
||||
type = "text"
|
||||
prompt = "Project Name"
|
||||
required = true
|
||||
default = "my-rust-project"
|
||||
nickel_path = ["project", "name"]
|
||||
|
||||
[[elements]]
|
||||
name = "project_description"
|
||||
type = "text"
|
||||
prompt = "Project Description"
|
||||
required = true
|
||||
default = "A Rust project with CI/CD"
|
||||
nickel_path = ["project", "description"]
|
||||
|
||||
[[elements]]
|
||||
name = "project_repository"
|
||||
type = "text"
|
||||
prompt = "Repository URL"
|
||||
required = true
|
||||
default = "https://github.com/example/my-rust-project"
|
||||
nickel_path = ["project", "repository"]
|
||||
|
||||
# =============================================================================
|
||||
# GITHUB ACTIONS CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
[[elements]]
|
||||
name = "github_header"
|
||||
type = "section_header"
|
||||
title = "🚀 GitHub Actions Configuration"
|
||||
border_top = true
|
||||
border_bottom = true
|
||||
|
||||
[[elements]]
|
||||
name = "enable_github_actions"
|
||||
type = "confirm"
|
||||
prompt = "Enable GitHub Actions CI?"
|
||||
default = true
|
||||
nickel_path = ["ci", "github_actions", "enabled"]
|
||||
|
||||
[[elements]]
|
||||
name = "parallel_jobs"
|
||||
type = "text"
|
||||
prompt = "Parallel Jobs"
|
||||
default = "4"
|
||||
when = "enable_github_actions == true"
|
||||
help = "Number of parallel CI jobs (1-20)"
|
||||
nickel_path = ["ci", "github_actions", "parallel_jobs"]
|
||||
|
||||
[[elements]]
|
||||
name = "timeout_minutes"
|
||||
type = "text"
|
||||
prompt = "Job Timeout (minutes)"
|
||||
default = "60"
|
||||
when = "enable_github_actions == true"
|
||||
help = "Maximum duration for each job"
|
||||
nickel_path = ["ci", "github_actions", "timeout_minutes"]
|
||||
|
||||
# =============================================================================
|
||||
# RUST CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
[[elements]]
|
||||
name = "rust_header"
|
||||
type = "section_header"
|
||||
title = "🦀 Rust Configuration"
|
||||
border_top = true
|
||||
border_bottom = true
|
||||
when = "enable_github_actions == true"
|
||||
|
||||
[[elements]]
|
||||
name = "rust_version"
|
||||
type = "select"
|
||||
prompt = "Rust Version"
|
||||
default = "stable"
|
||||
when = "enable_github_actions == true"
|
||||
options = [
|
||||
{ value = "stable", label = "Stable - Latest stable release" },
|
||||
{ value = "nightly", label = "Nightly - Cutting edge features" },
|
||||
{ value = "1.70.0", label = "1.70.0 - Specific version" },
|
||||
]
|
||||
nickel_path = ["ci", "github_actions", "rust", "version"]
|
||||
|
||||
# =============================================================================
|
||||
# CACHING
|
||||
# =============================================================================
|
||||
|
||||
[[elements]]
|
||||
name = "cache_header"
|
||||
type = "section_header"
|
||||
title = "💾 Build Cache"
|
||||
border_top = true
|
||||
border_bottom = true
|
||||
when = "enable_github_actions == true"
|
||||
|
||||
[[elements]]
|
||||
name = "enable_cache"
|
||||
type = "confirm"
|
||||
prompt = "Enable dependency caching?"
|
||||
default = false
|
||||
when = "enable_github_actions == true"
|
||||
help = "Cache Cargo dependencies to speed up builds"
|
||||
nickel_path = ["ci", "github_actions", "cache", "enabled"]
|
||||
|
||||
# =============================================================================
|
||||
# TOOLS CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
[[elements]]
|
||||
name = "tools_header"
|
||||
type = "section_header"
|
||||
title = "🔧 CI Tools"
|
||||
border_top = true
|
||||
border_bottom = true
|
||||
when = "enable_github_actions == true"
|
||||
|
||||
[[elements]]
|
||||
name = "enable_clippy"
|
||||
type = "confirm"
|
||||
prompt = "Enable cargo clippy (linter)?"
|
||||
default = true
|
||||
when = "enable_github_actions == true"
|
||||
help = "Run Rust linter on code"
|
||||
nickel_path = ["ci", "github_actions", "tools", "clippy", "enabled"]
|
||||
|
||||
[[elements]]
|
||||
name = "clippy_args"
|
||||
type = "text"
|
||||
prompt = "Clippy Arguments"
|
||||
default = "-D warnings"
|
||||
when = "enable_github_actions == true && enable_clippy == true"
|
||||
help = "Command-line arguments for clippy"
|
||||
nickel_path = ["ci", "github_actions", "tools", "clippy", "args"]
|
||||
|
||||
[[elements]]
|
||||
name = "enable_rustfmt"
|
||||
type = "confirm"
|
||||
prompt = "Enable rustfmt (formatter)?"
|
||||
default = true
|
||||
when = "enable_github_actions == true"
|
||||
help = "Check code formatting"
|
||||
nickel_path = ["ci", "github_actions", "tools", "rustfmt", "enabled"]
|
||||
|
||||
[[elements]]
|
||||
name = "rustfmt_edition"
|
||||
type = "select"
|
||||
prompt = "Rust Edition for rustfmt"
|
||||
default = "2021"
|
||||
when = "enable_github_actions == true && enable_rustfmt == true"
|
||||
options = [
|
||||
{ value = "2015", label = "2015" },
|
||||
{ value = "2018", label = "2018" },
|
||||
{ value = "2021", label = "2021" },
|
||||
]
|
||||
nickel_path = ["ci", "github_actions", "tools", "rustfmt", "edition"]
|
||||
|
||||
[[elements]]
|
||||
name = "enable_cargo_audit"
|
||||
type = "confirm"
|
||||
prompt = "Enable cargo-audit (security)?"
|
||||
default = false
|
||||
when = "enable_github_actions == true"
|
||||
help = "Scan for security vulnerabilities"
|
||||
nickel_path = ["ci", "github_actions", "tools", "cargo_audit", "enabled"]
|
||||
|
||||
# =============================================================================
|
||||
# DEPLOYMENT
|
||||
# =============================================================================
|
||||
|
||||
[[elements]]
|
||||
name = "deployment_header"
|
||||
type = "section_header"
|
||||
title = "🚢 Deployment"
|
||||
border_top = true
|
||||
border_bottom = true
|
||||
|
||||
[[elements]]
|
||||
name = "enable_deployment"
|
||||
type = "confirm"
|
||||
prompt = "Enable deployment?"
|
||||
default = false
|
||||
help = "Automatically deploy on successful builds"
|
||||
nickel_path = ["deployment", "enabled"]
|
||||
|
||||
[[elements]]
|
||||
name = "deployment_environment"
|
||||
type = "select"
|
||||
prompt = "Deployment Environment"
|
||||
default = "production"
|
||||
when = "enable_deployment == true"
|
||||
options = [
|
||||
{ value = "development", label = "Development" },
|
||||
{ value = "staging", label = "Staging" },
|
||||
{ value = "production", label = "Production" },
|
||||
]
|
||||
nickel_path = ["deployment", "environment"]
|
||||
|
||||
[[elements]]
|
||||
name = "auto_deploy"
|
||||
type = "confirm"
|
||||
prompt = "Auto-deploy on main branch?"
|
||||
default = false
|
||||
when = "enable_deployment == true"
|
||||
help = "Automatically deploy when main branch is updated"
|
||||
nickel_path = ["deployment", "auto_deploy"]
|
||||
|
||||
# =============================================================================
|
||||
# SUMMARY
|
||||
# =============================================================================
|
||||
|
||||
[[elements]]
|
||||
name = "summary_header"
|
||||
type = "section_header"
|
||||
title = "✅ Review & Save"
|
||||
border_top = true
|
||||
|
||||
[[elements]]
|
||||
name = "summary"
|
||||
type = "section"
|
||||
content = "Review your configuration above. Click Submit to save and see what changed."
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
# CI Configuration
|
||||
# Generated by TypeDialog - Edit via ci-configure.sh
|
||||
|
||||
{
|
||||
project = {
|
||||
name = "my-rust-project",
|
||||
description = "A Rust project with CI/CD",
|
||||
repository = "https://github.com/example/my-rust-project",
|
||||
},
|
||||
|
||||
ci = {
|
||||
github_actions = {
|
||||
enabled = true,
|
||||
parallel_jobs = 4,
|
||||
timeout_minutes = 60,
|
||||
|
||||
rust = {
|
||||
version = "stable",
|
||||
targets = ["x86_64-unknown-linux-gnu"],
|
||||
},
|
||||
|
||||
cache = {
|
||||
enabled = false,
|
||||
paths = [],
|
||||
},
|
||||
|
||||
tools = {
|
||||
clippy = {
|
||||
enabled = true,
|
||||
args = ["-D", "warnings"],
|
||||
},
|
||||
rustfmt = {
|
||||
enabled = true,
|
||||
edition = "2021",
|
||||
},
|
||||
cargo_audit = {
|
||||
enabled = false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
deployment = {
|
||||
enabled = false,
|
||||
environment = "production",
|
||||
auto_deploy = false,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
# CI Configuration
|
||||
# Generated by TypeDialog - Edit via ci-configure.sh
|
||||
|
||||
{
|
||||
project = {
|
||||
name = "{{ project_name }}",
|
||||
description = "{{ project_description }}",
|
||||
repository = "{{ project_repository }}",
|
||||
},
|
||||
|
||||
ci = {
|
||||
github_actions = {
|
||||
enabled = {{ enable_github_actions }},
|
||||
parallel_jobs = {{ parallel_jobs }},
|
||||
timeout_minutes = {{ timeout_minutes }},
|
||||
|
||||
rust = {
|
||||
version = "{{ rust_version }}",
|
||||
targets = ["x86_64-unknown-linux-gnu"],
|
||||
},
|
||||
|
||||
cache = {
|
||||
enabled = {{ enable_cache }},
|
||||
paths = [],
|
||||
},
|
||||
|
||||
tools = {
|
||||
clippy = {
|
||||
enabled = {{ enable_clippy }},
|
||||
{% if enable_clippy and clippy_args %}
|
||||
args = {{ clippy_args | split(pat=" ") | json }},
|
||||
{% else %}
|
||||
args = [],
|
||||
{% endif %}
|
||||
},
|
||||
rustfmt = {
|
||||
enabled = {{ enable_rustfmt }},
|
||||
edition = "{{ rustfmt_edition }}",
|
||||
},
|
||||
cargo_audit = {
|
||||
enabled = {{ enable_cargo_audit }},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
deployment = {
|
||||
enabled = {{ enable_deployment }},
|
||||
environment = "{{ deployment_environment }}",
|
||||
auto_deploy = {{ auto_deploy }},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,256 +0,0 @@
|
|||
# Conditional Logic Examples
|
||||
|
||||
This directory demonstrates all supported conditional operators in TypeDialog forms.
|
||||
|
||||
## Overview
|
||||
|
||||
TypeDialog supports rich conditional logic for dynamic form behavior. Fields can be shown/hidden based on previous answers using the `when` attribute.
|
||||
|
||||
## Running the Examples
|
||||
|
||||
### CLI Backend
|
||||
|
||||
```bash
|
||||
cargo run --bin typedialog -- examples/13-conditional-logic/conditional-demo.toml
|
||||
```
|
||||
|
||||
### TUI Backend
|
||||
|
||||
```bash
|
||||
cargo run --bin typedialog-tui -- examples/13-conditional-logic/conditional-demo.toml
|
||||
```
|
||||
|
||||
### Web Backend
|
||||
|
||||
```bash
|
||||
cargo run --bin typedialog-web -- examples/13-conditional-logic/conditional-demo.toml
|
||||
```
|
||||
|
||||
## Supported Operators
|
||||
|
||||
### Comparison Operators
|
||||
|
||||
#### Equality and Inequality
|
||||
|
||||
- `==`: Equal to
|
||||
- `!=`: Not equal to
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "mysql_config"
|
||||
when = "database_driver == mysql"
|
||||
|
||||
[[elements]]
|
||||
name = "server_warning"
|
||||
when = "database_driver != sqlite"
|
||||
```
|
||||
|
||||
#### Numeric Comparisons
|
||||
|
||||
- `>`: Greater than
|
||||
- `<`: Less than
|
||||
- `>=`: Greater than or equal
|
||||
- `<=`: Less than or equal
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "privileged_port_warning"
|
||||
when = "server_port < 1024"
|
||||
|
||||
[[elements]]
|
||||
name = "high_port_warning"
|
||||
when = "server_port > 10000"
|
||||
```
|
||||
|
||||
### String Operators
|
||||
|
||||
- `contains`: Check if field contains substring
|
||||
- `startswith`: Check if field starts with prefix
|
||||
- `endswith`: Check if field ends with suffix
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "https_notice"
|
||||
when = "project_url startswith https"
|
||||
|
||||
[[elements]]
|
||||
name = "github_specific"
|
||||
when = "project_url endswith github.com"
|
||||
|
||||
[[elements]]
|
||||
name = "gitlab_ci"
|
||||
when = "project_url contains gitlab"
|
||||
```
|
||||
|
||||
### Array Membership
|
||||
|
||||
- `in`: Check if value exists in array field
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "rust_toolchain"
|
||||
when = "rust in detected_languages"
|
||||
|
||||
[[elements]]
|
||||
name = "python_venv"
|
||||
when = "python in detected_languages"
|
||||
```
|
||||
|
||||
**Works with**:
|
||||
|
||||
- MultiSelect fields (JSON array or comma-separated string)
|
||||
- Array values from JSON output
|
||||
|
||||
### File System Conditions
|
||||
|
||||
- `file_exists(path)`: Check if file or directory exists
|
||||
- `!file_exists(path)`: Check if file does NOT exist (negation)
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "dockerfile_exists_notice"
|
||||
when = "file_exists(Dockerfile)"
|
||||
|
||||
[[elements]]
|
||||
name = "create_dockerfile"
|
||||
when = "!file_exists(Dockerfile)"
|
||||
|
||||
[[elements]]
|
||||
name = "env_setup"
|
||||
type = "group"
|
||||
includes = ["fragments/environment-setup.toml"]
|
||||
when = "!file_exists(.env)"
|
||||
```
|
||||
|
||||
**Path resolution**:
|
||||
|
||||
- Absolute paths: `/etc/config.toml`
|
||||
- Relative paths: `Dockerfile`, `.env`, `config/settings.toml`
|
||||
- Works with both files and directories
|
||||
|
||||
## Form Flow Example
|
||||
|
||||
1. **Select database** → Shows driver-specific fields
|
||||
2. **Enter port number** → Shows warnings based on port range
|
||||
3. **Enter project URL** → Shows platform-specific options
|
||||
4. **Select languages** → Shows language-specific tooling
|
||||
5. **Check for files** → Conditionally load setup fragments
|
||||
|
||||
## Key Features
|
||||
|
||||
### Dynamic Field Visibility
|
||||
|
||||
Fields appear/disappear based on user input:
|
||||
|
||||
```toml
|
||||
# Only shows if user selects MySQL
|
||||
[[elements]]
|
||||
name = "mysql_password"
|
||||
type = "password"
|
||||
when = "database_driver == mysql"
|
||||
```
|
||||
|
||||
### Conditional Fragment Loading
|
||||
|
||||
Load entire form sections conditionally:
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "docker_setup"
|
||||
type = "group"
|
||||
includes = ["fragments/docker-init.toml"]
|
||||
when = "!file_exists(Dockerfile)"
|
||||
```
|
||||
|
||||
### Multi-Condition Fields
|
||||
|
||||
Same field can have complex logic (future: `&&` and `||`):
|
||||
|
||||
```toml
|
||||
# Current: Single condition per field
|
||||
when = "rust in detected_languages"
|
||||
|
||||
# Future: Compound conditions
|
||||
when = "(rust in detected_languages) && (server_port >= 1024)"
|
||||
```
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Scenario 1: Database Configuration
|
||||
|
||||
1. Select `mysql` → See MySQL-specific fields
|
||||
2. Select `postgresql` → See PostgreSQL-specific fields
|
||||
3. Select `sqlite` → No extra server fields
|
||||
|
||||
### Scenario 2: Port Validation
|
||||
|
||||
1. Enter `80` → See privileged port warning (`< 1024`)
|
||||
2. Enter `8080` → See standard port notice (`>= 1024`)
|
||||
3. Enter `15000` → See high port warning (`> 10000`)
|
||||
|
||||
### Scenario 3: Language Detection
|
||||
|
||||
1. Select `rust` + `python` → See both Rust and Python config fields
|
||||
2. Select only `javascript` → See only Node.js version selector
|
||||
3. Select none → No language-specific fields appear
|
||||
|
||||
### Scenario 4: File System Checks
|
||||
|
||||
1. **With Dockerfile present**:
|
||||
- Shows "Dockerfile found" notice
|
||||
- Skips Docker setup wizard
|
||||
|
||||
2. **Without Dockerfile**:
|
||||
- Asks to create Dockerfile
|
||||
- Shows Docker setup group
|
||||
|
||||
3. **With .env file**:
|
||||
- Asks to use existing config
|
||||
- Skips environment setup
|
||||
|
||||
4. **Without .env file**:
|
||||
- Loads environment setup fragment
|
||||
- Prompts for all env variables
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Condition Evaluation
|
||||
|
||||
Conditions are evaluated at runtime during form execution:
|
||||
|
||||
1. User answers a field
|
||||
2. System checks all pending fields for `when` conditions
|
||||
3. Fields with satisfied conditions become visible
|
||||
4. Fields with unsatisfied conditions remain hidden
|
||||
|
||||
### Fragment Loading
|
||||
|
||||
Conditional groups with `includes` load fragments only when condition is true:
|
||||
|
||||
```toml
|
||||
[[elements]]
|
||||
name = "advanced_config"
|
||||
type = "group"
|
||||
includes = ["fragments/advanced-settings.toml"]
|
||||
when = "enable_advanced == true"
|
||||
```
|
||||
|
||||
This prevents loading unnecessary form definitions until needed.
|
||||
|
||||
### Type Conversion
|
||||
|
||||
TypeDialog automatically handles type conversions in conditions:
|
||||
|
||||
- String `"8080"` compared to number `1024` → converted to number
|
||||
- Boolean `true` compared to string `"true"` → compared as boolean
|
||||
- Number compared to string number → compared numerically
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Field Types Reference](../../docs/field_types.md) - All field types and attributes
|
||||
- [Fragment System](../../docs/fragment-search-paths.md) - Dynamic form composition
|
||||
- [Nickel Integration](../../docs/nickel.md) - Schema-driven forms
|
||||
|
||||
## Source Code
|
||||
|
||||
Condition evaluation logic: `crates/typedialog-core/src/form_parser/conditions.rs`
|
||||
|
|
@ -1,198 +0,0 @@
|
|||
# Conditional Logic Demo
|
||||
# Demonstrates all supported conditional operators in TypeDialog
|
||||
|
||||
name = "conditional_demo"
|
||||
description = "Complete demonstration of conditional field visibility"
|
||||
|
||||
# ====================
|
||||
# COMPARISON OPERATORS
|
||||
# ====================
|
||||
|
||||
[[elements]]
|
||||
name = "database_driver"
|
||||
type = "select"
|
||||
prompt = "Select database driver"
|
||||
required = true
|
||||
options = [
|
||||
{ value = "sqlite", label = "SQLite (embedded)" },
|
||||
{ value = "mysql", label = "MySQL" },
|
||||
{ value = "postgresql", label = "PostgreSQL" }
|
||||
]
|
||||
|
||||
# Equality (==)
|
||||
[[elements]]
|
||||
name = "mysql_config"
|
||||
type = "text"
|
||||
prompt = "MySQL connection string"
|
||||
when = "database_driver == mysql"
|
||||
placeholder = "mysql://localhost:3306/db"
|
||||
|
||||
# Inequality (!=)
|
||||
[[elements]]
|
||||
name = "server_warning"
|
||||
type = "section"
|
||||
content = "⚠️ You selected a server-based database. Ensure the server is running."
|
||||
when = "database_driver != sqlite"
|
||||
|
||||
# ====================
|
||||
# NUMERIC COMPARISONS
|
||||
# ====================
|
||||
|
||||
[[elements]]
|
||||
name = "server_port"
|
||||
type = "text"
|
||||
prompt = "Server port"
|
||||
default = "8080"
|
||||
required = true
|
||||
|
||||
# Greater than (>)
|
||||
[[elements]]
|
||||
name = "high_port_warning"
|
||||
type = "section"
|
||||
content = "⚠️ Port > 10000 is uncommon. Double-check your configuration."
|
||||
when = "server_port > 10000"
|
||||
|
||||
# Less than (<)
|
||||
[[elements]]
|
||||
name = "privileged_port_warning"
|
||||
type = "section"
|
||||
content = "⚠️ Port < 1024 requires root/admin privileges."
|
||||
when = "server_port < 1024"
|
||||
|
||||
# Greater than or equal (>=)
|
||||
[[elements]]
|
||||
name = "standard_port_notice"
|
||||
type = "section"
|
||||
content = "✓ Using standard user port range (>= 1024)"
|
||||
when = "server_port >= 1024"
|
||||
|
||||
# Less than or equal (<=)
|
||||
[[elements]]
|
||||
name = "low_port_range"
|
||||
type = "section"
|
||||
content = "Using low port range (<= 5000)"
|
||||
when = "server_port <= 5000"
|
||||
|
||||
# ====================
|
||||
# STRING OPERATORS
|
||||
# ====================
|
||||
|
||||
[[elements]]
|
||||
name = "project_url"
|
||||
type = "text"
|
||||
prompt = "Project repository URL"
|
||||
placeholder = "https://github.com/user/repo"
|
||||
|
||||
# startswith
|
||||
[[elements]]
|
||||
name = "https_notice"
|
||||
type = "section"
|
||||
content = "✓ Secure HTTPS URL detected"
|
||||
when = "project_url startswith https"
|
||||
|
||||
# endswith
|
||||
[[elements]]
|
||||
name = "github_specific"
|
||||
type = "text"
|
||||
prompt = "GitHub Actions enabled?"
|
||||
when = "project_url endswith github.com"
|
||||
|
||||
# contains
|
||||
[[elements]]
|
||||
name = "gitlab_ci"
|
||||
type = "confirm"
|
||||
prompt = "Enable GitLab CI integration?"
|
||||
when = "project_url contains gitlab"
|
||||
|
||||
# ====================
|
||||
# ARRAY MEMBERSHIP (in)
|
||||
# ====================
|
||||
|
||||
[[elements]]
|
||||
name = "detected_languages"
|
||||
type = "multiselect"
|
||||
prompt = "Which languages are used in your project?"
|
||||
display_mode = "grid"
|
||||
options = [
|
||||
{ value = "rust", label = "🦀 Rust" },
|
||||
{ value = "python", label = "🐍 Python" },
|
||||
{ value = "javascript", label = "📜 JavaScript" },
|
||||
{ value = "go", label = "🐹 Go" }
|
||||
]
|
||||
|
||||
# Array membership check
|
||||
[[elements]]
|
||||
name = "rust_toolchain"
|
||||
type = "select"
|
||||
prompt = "Rust toolchain version"
|
||||
when = "rust in detected_languages"
|
||||
options = [
|
||||
{ value = "stable", label = "Stable" },
|
||||
{ value = "nightly", label = "Nightly" },
|
||||
{ value = "beta", label = "Beta" }
|
||||
]
|
||||
|
||||
[[elements]]
|
||||
name = "python_venv"
|
||||
type = "confirm"
|
||||
prompt = "Use Python virtual environment?"
|
||||
when = "python in detected_languages"
|
||||
default = true
|
||||
|
||||
[[elements]]
|
||||
name = "nodejs_version"
|
||||
type = "select"
|
||||
prompt = "Node.js version"
|
||||
when = "javascript in detected_languages"
|
||||
options = [
|
||||
{ value = "18", label = "Node.js 18 LTS" },
|
||||
{ value = "20", label = "Node.js 20 LTS" },
|
||||
{ value = "latest", label = "Latest" }
|
||||
]
|
||||
|
||||
# ====================
|
||||
# FILE SYSTEM CONDITIONS
|
||||
# ====================
|
||||
|
||||
# file_exists(path)
|
||||
[[elements]]
|
||||
name = "dockerfile_exists_notice"
|
||||
type = "section"
|
||||
content = "✓ Dockerfile found in current directory"
|
||||
when = "file_exists(Dockerfile)"
|
||||
|
||||
# !file_exists(path) - negation
|
||||
[[elements]]
|
||||
name = "create_dockerfile"
|
||||
type = "confirm"
|
||||
prompt = "No Dockerfile found. Create one?"
|
||||
when = "!file_exists(Dockerfile)"
|
||||
default = true
|
||||
|
||||
[[elements]]
|
||||
name = "use_existing_config"
|
||||
type = "confirm"
|
||||
prompt = "Existing .env file found. Use existing configuration?"
|
||||
when = "file_exists(.env)"
|
||||
|
||||
[[elements]]
|
||||
name = "env_setup"
|
||||
type = "group"
|
||||
includes = ["fragments/environment-setup.toml"]
|
||||
when = "!file_exists(.env)"
|
||||
|
||||
# ====================
|
||||
# COMBINED CONDITIONS
|
||||
# ====================
|
||||
|
||||
[[elements]]
|
||||
name = "rust_docker_setup"
|
||||
type = "section"
|
||||
content = "🦀 Rust + Docker detected. Consider using rust:alpine base image."
|
||||
when = "rust in detected_languages"
|
||||
|
||||
[[elements]]
|
||||
name = "production_ready_check"
|
||||
type = "section"
|
||||
content = "✅ Production-ready configuration detected (HTTPS + standard port)"
|
||||
when = "server_port >= 1024"
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
# Environment Setup Fragment
|
||||
# Loaded conditionally when .env file doesn't exist
|
||||
|
||||
name = "environment_setup"
|
||||
description = "Environment variables configuration"
|
||||
display_mode = "complete"
|
||||
|
||||
[[elements]]
|
||||
name = "env_header"
|
||||
type = "section_header"
|
||||
title = "🔧 Environment Configuration"
|
||||
border_top = true
|
||||
border_bottom = true
|
||||
|
||||
[[elements]]
|
||||
name = "env_mode"
|
||||
type = "select"
|
||||
prompt = "Environment mode"
|
||||
required = true
|
||||
options = [
|
||||
{ value = "development", label = "Development" },
|
||||
{ value = "staging", label = "Staging" },
|
||||
{ value = "production", label = "Production" }
|
||||
]
|
||||
|
||||
[[elements]]
|
||||
name = "log_level"
|
||||
type = "select"
|
||||
prompt = "Log level"
|
||||
default = "info"
|
||||
options = [
|
||||
{ value = "trace", label = "Trace (verbose)" },
|
||||
{ value = "debug", label = "Debug" },
|
||||
{ value = "info", label = "Info" },
|
||||
{ value = "warn", label = "Warning" },
|
||||
{ value = "error", label = "Error" }
|
||||
]
|
||||
|
||||
[[elements]]
|
||||
name = "debug_mode"
|
||||
type = "confirm"
|
||||
prompt = "Enable debug mode?"
|
||||
when = "env_mode == development"
|
||||
default = true
|
||||
|
||||
[[elements]]
|
||||
name = "api_key"
|
||||
type = "password"
|
||||
prompt = "API Key"
|
||||
required = true
|
||||
help = "Your application API key"
|
||||
|
||||
[[elements]]
|
||||
name = "database_url"
|
||||
type = "text"
|
||||
prompt = "Database URL"
|
||||
placeholder = "postgresql://localhost:5432/mydb"
|
||||
required = true
|
||||
|
||||
[[elements]]
|
||||
name = "redis_url"
|
||||
type = "text"
|
||||
prompt = "Redis URL"
|
||||
placeholder = "redis://localhost:6379"
|
||||
when = "env_mode == production"
|
||||
|
||||
[[elements]]
|
||||
name = "sentry_dsn"
|
||||
type = "text"
|
||||
prompt = "Sentry DSN (error tracking)"
|
||||
when = "env_mode == production"
|
||||
help = "Leave empty to disable error tracking"
|
||||
|
|
@ -189,41 +189,6 @@ cat examples/12-agent-execution/README.md
|
|||
- [Tests](../tests/agent/) - Agent validation tests
|
||||
- [Core Examples](../crates/typedialog-agent/typedialog-ag-core/examples/) - Rust API usage
|
||||
|
||||
### 13. **Conditional Logic** → [`13-conditional-logic/`](13-conditional-logic/)
|
||||
|
||||
Complete guide to all conditional operators and expressions.
|
||||
|
||||
**Operators demonstrated:**
|
||||
|
||||
| Category | Operators | Example |
|
||||
|----------|-----------|---------|
|
||||
| **Comparison** | `==`, `!=`, `>`, `<`, `>=`, `<=` | `port >= 1024` |
|
||||
| **String** | `contains`, `startswith`, `endswith` | `url startswith https` |
|
||||
| **Array** | `in` | `rust in detected_languages` |
|
||||
| **File System** | `file_exists()`, `!file_exists()` | `!file_exists(Dockerfile)` |
|
||||
|
||||
**Features:**
|
||||
|
||||
- Dynamic field visibility based on user input
|
||||
- Conditional fragment loading
|
||||
- Multi-condition scenarios
|
||||
- Type-safe comparisons
|
||||
|
||||
**Running the example:**
|
||||
|
||||
```bash
|
||||
# CLI
|
||||
cargo run --bin typedialog -- examples/13-conditional-logic/conditional-demo.toml
|
||||
|
||||
# TUI
|
||||
cargo run --bin typedialog-tui -- examples/13-conditional-logic/conditional-demo.toml
|
||||
|
||||
# Web
|
||||
cargo run --bin typedialog-web -- examples/13-conditional-logic/conditional-demo.toml
|
||||
```
|
||||
|
||||
**Best for:** Dynamic forms, adaptive UX, configuration wizards
|
||||
|
||||
## Learning Path
|
||||
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue