convert_case-0.11.0/.cargo_vcs_info.json0000644000000001360000000000100135660ustar { "git": { "sha1": "3ca58fcf557f6a42576c7b63e0780f4cf823c40a" }, "path_in_vcs": "" }convert_case-0.11.0/.envrc000064400000000000000000000000121046102023000134650ustar 00000000000000use flake convert_case-0.11.0/.gitignore000064400000000000000000000000761046102023000143510ustar 00000000000000/target /ccase/test/tmp cobertura.xml /result .vscode .direnv convert_case-0.11.0/Cargo.lock0000644000000463730000000000100115560ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anes" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "ciborium" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", "serde", ] [[package]] name = "ciborium-io" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" [[package]] name = "ciborium-ll" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" dependencies = [ "ciborium-io", "half", ] [[package]] name = "clap" version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstyle", "clap_lex", ] [[package]] name = "clap_lex" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "convert_case" version = "0.11.0" dependencies = [ "criterion", "rstest", "unicode-segmentation", ] [[package]] name = "criterion" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", "cast", "ciborium", "clap", "criterion-plot", "is-terminal", "itertools", "num-traits", "once_cell", "oorandom", "plotters", "rayon", "regex", "serde", "serde_derive", "serde_json", "tinytemplate", "walkdir", ] [[package]] name = "criterion-plot" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", "itertools", ] [[package]] name = "crossbeam-deque" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-macro", "futures-task", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "half" version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ "cfg-if", "crunchy", "zerocopy", ] [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "indexmap" version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "is-terminal" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", "windows-sys", ] [[package]] name = "itertools" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" dependencies = [ "either", ] [[package]] name = "itoa" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oorandom" version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "plotters" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", "plotters-svg", "wasm-bindgen", "web-sys", ] [[package]] name = "plotters-backend" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] [[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "rayon" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", ] [[package]] name = "rayon-core" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] [[package]] name = "regex" version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "relative-path" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rstest" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03e905296805ab93e13c1ec3a03f4b6c4f35e9498a3d5fa96dc626d22c03cd89" dependencies = [ "futures-timer", "futures-util", "rstest_macros", "rustc_version", ] [[package]] name = "rstest_macros" version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef0053bbffce09062bee4bcc499b0fbe7a57b879f1efe088d6d8d4c7adcdef9b" dependencies = [ "cfg-if", "glob", "proc-macro-crate", "proc-macro2", "quote", "regex", "relative-path", "rustc_version", "syn", "unicode-ident", ] [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ "winapi-util", ] [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", "serde_core", ] [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "syn" version = "2.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tinytemplate" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ "serde", "serde_json", ] [[package]] name = "toml_datetime" version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", "toml_datetime", "toml_parser", "winnow", ] [[package]] name = "toml_parser" version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "walkdir" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", ] [[package]] name = "wasm-bindgen" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" version = "0.3.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "winapi-util" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ "windows-sys", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "winnow" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] [[package]] name = "zerocopy" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", "syn", ] convert_case-0.11.0/Cargo.toml0000644000000025050000000000100115660ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "convert_case" version = "0.11.0" authors = ["rutrum "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Convert strings into any case" readme = "README.md" keywords = [ "casing", "case", "string", ] categories = ["text-processing"] license = "MIT" repository = "https://github.com/rutrum/convert-case" [lib] name = "convert_case" path = "src/lib.rs" [[test]] name = "macros" path = "tests/macros.rs" [[test]] name = "string_types" path = "tests/string_types.rs" [[bench]] name = "convert" path = "benches/convert.rs" harness = false [dependencies.unicode-segmentation] version = "1.9.0" [dev-dependencies.criterion] version = "0.5" [dev-dependencies.rstest] version = "0.24" [profile.release] lto = true codegen-units = 1 panic = "abort" convert_case-0.11.0/Cargo.toml.orig000064400000000000000000000011641046102023000152470ustar 00000000000000[package] name = "convert_case" version = "0.11.0" authors = ["rutrum "] edition = "2021" description = "Convert strings into any case" license = "MIT" keywords = [ "casing", "case", "string" ] categories = [ "text-processing" ] readme = "README.md" repository = "https://github.com/rutrum/convert-case" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [profile.release] codegen-units = 1 lto = true panic = 'abort' [dependencies] unicode-segmentation = "1.9.0" [dev-dependencies] criterion = "0.5" rstest = "0.24" [[bench]] name = "convert" harness = false convert_case-0.11.0/LICENSE000064400000000000000000000020461046102023000133650ustar 00000000000000MIT License Copyright (c) 2025 rutrum Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.convert_case-0.11.0/README.md000064400000000000000000000276451046102023000136530ustar 00000000000000# `convert-case` Rust library for converting between string cases. ```{rust} use convert_case::ccase; assert_eq!( ccase!(camel, "My_Var_Name"), "myVarName", ); assert_eq!( ccase!(snake, "IOStream"), "io_stream", ); assert_eq!( ccase!(snake -> title, "2020-04-16_family_photo"), "2020-04-16 Family Photo", ); ``` `convert-case` is highly customizable. You can read the API documentation on [docs.rs](https://docs.rs/convert_case/) for a list of all features and read lots of examples. ## Cases This is list of cases that `convert_case` provides out of the box. You can always make your own custom case. | Case | Example | | ---- | ------- | | Snake | `my_variable_name` | | Constant
UpperSnake | `MY_VARIABLE_NAME` | | Ada | `My_Variable_Name` | | Kebab | `my-variable-name` | | Cobol
UpperKebab | `MY-VARIABLE-NAME` | | Train | `My-Variable-Name` | | Flat | `myvariablename` | | UpperFlat | `MYVARIABLENAME` | | Pascal
UpperCamel | `MyVariableName` | | Camel | `myVariableName` | | Upper | `MY VARIABLE NAME` | | Lower | `my variable name` | | Title | `My Variable Name` | | Sentence | `My variable name` | ## Additional utilities with `convert_case_extras` Some cases and utilities that didn't feel appropriate in this library are made available in a distinct crate called [`convert_case_extras`](https://github.com/rutrum/convert-case-extras). This crate is a demonstration of what can be built on top of the `convert_case` API. ## Command Line Utility `ccase` The [command line utility `ccase`](https://github.com/rutrum/ccase) was made to expose the tools of the `convert_case` library to the command line. ``` $ ccase -t title super_mario_64 Super Mario 64 $ ccase -f snake -t title 2020-04-15_my_cat 2020-04-16 My Cat $ ccase -t camel "convert to camel" convertToCamel ``` ## Links | | `convert_case` | `convert_case_extras` | `ccase` | | --- | --- | --- | --- | | Repository | [github](https://github.com/rutrum/convert-case) | [github](https://github.com/rutrum/convert-case-extras) | [github](https://github.com/rutrum/ccase) | | Crate | [crates.io](https://crates.io/crates/convert_case) | [crates.io](https://crates.io/crates/convert_case_extras) | [crates.io](https://crates.io/crates/ccase) | | Documentation | [docs.rs](https://docs.rs/convert_case) | [docs.rs](https://docs.rs/convert_case_extras) | | ## Change Log ### 0.11.0: Multiple Patterns The headline change permits trailing, leading, and duplicate delimiters to persist (default) or be removed. Instead of applying a single pattern, `Converter` supports multiple patterns and will apply them in order. This comes with another pattern `Pattern::RemoveEmpty` that drops empty words. This allows for opting into what was default behavior in 0.8.0 and before. New features: * `Converter` now supports multiple patterns via `Converter.patterns: Vec`. Patterns are applied in sequence, allowing for composition. * `Pattern::RemoveEmpty` filters out empty words. This is useful when splitting produces empty words from leading, trailing, or duplicate delimiters (e.g., `"--a--b"` split on hyphens). * `Casing::remove_empty()` is a convenience method that adds `Pattern::RemoveEmpty` to the `patterns` vector. ```rust "--leading-delims".remove_empty().to_case(Case::Camel) // "leadingDelims" ``` Breaking changes: * `delim_boundary` macro is now `separator` * Removed `Casing::is_case`. Similar functionality is now implemented in `convert-case-extras`. * Instances of `delim` have been renamed to `delimiter`: * `Converter.delim` -> `Converter.delimiter` * `Converter::set_delim` -> `Converter::set_delimiter` * `Case::Custom.delim` -> `Case::Custom.delimiter` * `Case::delim` -> `Case::delimiter` * `Converter.pattern` is now `Converter.patterns` with type `Vec` * New pattern methods on `Converter` just like boundaries: `set_patterns`, `add_pattern`, `add_patterns`, `remove_pattern`, `remove_patterns`. * `Pattern::Noop` removed. An empty patterns vector represents no mutation. * `Boundary::Custom` and `Pattern::Custom` variants are no longer comparable. * Implementations of `PartialEq`, `Eq`, and `Hash` are no longer derived on `Boundary` and `Pattern`, and have been manually implemented. * Custom variants always return `false` for equality checks because function pointers cannot be reliably compared. This has been true for all of rust, but the compiler warning is relatively new to the author. This means `remove_boundary` and `remove_pattern` will not work for custom variants. * Further, all `Custom` variants now hash to the same value. Other changes: * Much improved documentation: fixed typos, better examples. * More comprehensive testing using `rstest`. ### 0.10.0: More clean up to prepare for 1.0.0 Since the library is so extensible with its new API, there is no longer a need for some niche or fun transformations to be made available in this library. Some of the features that are removed are now in a new library `convert_case_extras`. That library will have a lower threshold on what is included (i.e. more features), and will also serve as a demonstration of what's capable with the `convert_case` API. Removed: * `Case::Toggle` and `Pattern::Toggle` * `Case::Alternating` and `Pattern::Alternating` * `Case::Random` and `Pattern::Random` * `Case::PseudoRandom` and `Pattern::PseudoRandom` * `random` feature is removed. The library no longer has any features. * `Case::deterministic_cases` is removed * `Case::random_cases` is removed Other breaking changes: * `Boundary::Custom` has lost the `arg` parameter and `Boundary::Custom.condition` is more simply `fn(&[&str]) -> bool`. * `arg` was originally used for building boundaries from delimiters with the `Boundary::from_delim` function, which is also removed because * `delim_boundary!` macro has replaced `Boundary::from_delim` functionality, without the need of the `arg` parameters * `Casing::with_boundaries` is now `Casing::set_boundaries` and `Casing::without_boundaries` is now `Casing::remove_boundaries` to align with `Converter` ### 0.9.0: Back to enums, but keep the customization This release is trying to finalize the API for a 1.0.0 release. In hindsight, making `Pattern` a function and `Boundary` a struct were poor decisions. While trying to add customized behavior, I made it harder to use. Luckily, by following the convention that was done with `Case` in 0.8.0, which was providing a struct-variant `Case::Custom`, I can keep the new customization options while reverting back to the enum pattern the library has had since the beginning. If you are updating from before 0.7.0, I don't think any changes are necessary. If you're coming from 0.7.0 or higher, * Change boundaries from constants to enum variants: `Boundary::UPPER_SNAKE` into `Boundary::UpperSnake` * Change patterns from functions to enum variants on `Pattern`: `pattern::lowercase` into `Pattern::Lowercase` Most excitingly, I've introduced a brand new way to use the crate for 99% of use cases, which is the default behavior out of the box to just convert a string to a case, with no modifications to behavior. Instead of ``` use convert_case::{Case, Casing}; "MyVarName".to_case(Case::Snake); "my-var name".from_case(Case::Title).to_case(Case::Snake); ``` You can use the `ccase!` macro: ``` use convert_case::ccase; ccase!(snake, "MyVarName"); ccase!(title -> snake, "my-var name"); ``` This means only one import and its invocation is brief and easy to read. This is now the first thing you see when viewing the docs, which themselves have gotten a huge overhaul and many improvements in this version. New features: * `ccase!` macro that performs case conversion on a string _without needing to import `Case` or `Casing`_. It has two forms: * `ccase!(snake, "string")` is equivalent to `"string".to_case(Case::Snake)` * `ccase!(kebab -> snake, "string")` is equivalent to `"string".from_case(Case::Kebab).to_case(Case::Snake)` * While not intended to be used directly, the new `case!` macro returns a `Case` variant from the snake case version of the variant. For instance, `case!(snake)` is substituted for `Case::Snake` and `case!(upper_flat)` for `Case::UpperFlat`. Other breaking changes: * Leading, trailing, and duplicate delimiters are no longer removed and are returned as is. * `Boundary` is reverted back to being an enum, but with a `Custom` variant that gives all the same flexibility that `Boundary` had as a struct. This aligns with the `Case::Custom` pattern. * `Boundary.match` will return true if a set of graphemes matches the boundary condition * `Boundary` methods for `start` and `len` describe how enum variants consume letters when matched * `Pattern` is reverted back to being an enum, but with a `Custom` variant that allowed you to pass your own `fn (&[&str]) -> Vec` as input. * `Pattern.mutate` will perform the associated mutation function * `Converter.with_boundaries` has been renamed to `Converter.set_boundaries`. * Removed `Converter.remove_delim` and `Converter.remove_pattern`. You can use `set_delim("")` and `set_pattern(Pattern::Noop)` instead. * `ToString` is no longer a required trait for using `is_case` * `word_pattern` module has been removed ### 0.8.0: Pattern Overhaul, Custom Case Pattern is no longer an enum. It is now a type alias for `fn(&[&str]) -> Vec`. The variants of Pattern can now be referenced as functions inside the `pattern` module. For upgrading this means changing `Pattern::Lowercase` to `pattern::lowercase`, and calling the function directly instead of invoking the `mutate` method on the enum. Inside the pattern module is also the type alias `Pattern` itself. Other breaking changes: * Add `Case::Ada` (capital pattern with underscore delimiter.) * Add `Case::Custom` variant. It is a struct variant that takes three parameters: * pattern with type `Pattern` * delim with type `&static str`, and * boundaries with type `&'static [Boundary]`. * Because of the new `pattern::noop` function, `Converter` attribute `pattern` is now of type `Pattern` and not `Option` * `Case::deterministic_cases`, `Case::all_cases`, and `Case::random_cases` now return static arrays instead of vecs Other changes: * Added `Case::split`, `Case::mutate`, and `Case::join` which expose operations related to the boundaries, pattern, and delimiter of a case * Is now `no_std` compatible ### 0.7.1 * Removed debug print statement. ### 0.7.0: Custom Boundaries Boundary is no longer an enum. It now is a struct, and each enum variant corresponds to an associated constant. For upgrading this just means changing `Boundary::LowerUpper` to just `Boundary::LOWER_UPPER`. The benefit of this is that you can make your boundary conditions now, by instantiating the `Boundary` struct, or using `Boundary::from_delim()`. Now you can split on newlines, periods, double-colons, emojis, or a more complex case like a symbol followed by a digit. You also define which characters, if any, are removed during segmentation, and where the split happens. Changes from this feature: * Previous `Boundary::PascalName` enum variants now much referred to as `Boundary::CONSTANT_NAME` constants. * All functions that returned groups of boundaries (such as `Boundary::defaults()`, `Boundary::digit_letter()`, etc) now are const and return fixed-sized arrays `[Boundary; N]`, not `Vec`. * `Boundary::all()` was removed, since there's no longer a sense of "all" boundaries, since you can create your own. * `Boundary::list_from()` has been renamed to `Boundary::defaults_from()` and no longer outputs `Boundary::UPPER_LOWER`, since this function now only checks default boundaries. * Create custom delimiter boundaries using `Boundary::from_delim()`. Other breaking changes: * Rename `Case::ScreamingSnake` to `Case::Constant`. * Add `Case::Sentence` (sentence pattern and space delimiter.) * `Casing` trait implemented for `Arc` and `Rc` again. Other changes: * Remove most imports from doc comments. * Remove loop over `str::chars` in favor of `graphemes` from `unicode-segmentation`. convert_case-0.11.0/benches/convert.rs000064400000000000000000000014361046102023000160170ustar 00000000000000use convert_case::{Case, Casing}; use criterion::{criterion_group, criterion_main, Criterion}; fn criterion_benchmark(c: &mut Criterion) { c.bench_function("from-to", |b| { b.iter(|| { let words = vec![ "iGetUp", "and-nothing-gets-me-down", "YOUGotItTough", "i'veSeen_the_toughest_around", ]; for word in words { for to_case in Case::all_cases() { for from_case in Case::all_cases() { word.from_case(*from_case).to_case(*to_case); } word.to_case(*to_case); } } }) }); } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); convert_case-0.11.0/flake.lock000064400000000000000000000010671046102023000143160ustar 00000000000000{ "nodes": { "nixpkgs": { "locked": { "lastModified": 1734119587, "narHash": "sha256-AKU6qqskl0yf2+JdRdD0cfxX4b9x3KKV5RqA6wijmPM=", "owner": "nixos", "repo": "nixpkgs", "rev": "3566ab7246670a43abd2ffa913cc62dad9cdf7d5", "type": "github" }, "original": { "owner": "nixos", "ref": "nixos-unstable", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { "nixpkgs": "nixpkgs" } } }, "root": "root", "version": 7 } convert_case-0.11.0/flake.nix000064400000000000000000000014751046102023000141670ustar 00000000000000{ description = "convert-case flake"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; }; outputs = { self, nixpkgs }: let name = "convert-case"; system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; lib = pkgs.lib; in { packages.${system}.default = pkgs.rustPlatform.buildRustPackage { pname = name; version = (lib.importTOML ./Cargo.toml).package.version; src = ./.; cargoLock = { lockFile = ./Cargo.lock; }; }; devShells.${system}.default = pkgs.mkShell { name = "convert-case"; buildInputs = with pkgs; [ just watchexec rustup rust-analyzer typos ]; shellHook = ''just --list''; }; }; } convert_case-0.11.0/justfile000064400000000000000000000007401046102023000141270ustar 00000000000000test *FILTER: cargo fmt cargo build cargo test -q {{FILTER}} watch *FILTER: watchexec -e rs -rc reset -- just test {{FILTER}} build: cargo build --all watch-build: watchexec -- "reset && just build" doc: cargo doc --all-features open-doc: xdg-open target/doc/convert_case/index.html watch-doc: watchexec -- "just doc && cargo test --all-features --doc" tree: tree -I target verify-nostd: cargo build --target thumbv6m-none-eabi convert_case-0.11.0/rust-toolchain.toml000064400000000000000000000000371046102023000162260ustar 00000000000000[toolchain] channel = "1.91.1" convert_case-0.11.0/src/boundary.rs000064400000000000000000000437061046102023000153500ustar 00000000000000use unicode_segmentation::UnicodeSegmentation; use alloc::vec::Vec; fn grapheme_is_digit(c: &&str) -> bool { c.chars().all(|c| c.is_ascii_digit()) } fn grapheme_is_uppercase(c: &&str) -> bool { c.to_uppercase() != c.to_lowercase() && *c == c.to_uppercase() } fn grapheme_is_lowercase(c: &&str) -> bool { c.to_uppercase() != c.to_lowercase() && *c == c.to_lowercase() } /// Conditions for splitting an identifier into words. /// /// Some boundaries, [`Hyphen`](Boundary::Hyphen), [`Underscore`](Boundary::Underscore), and [`Space`](Boundary::Space), /// consume the character they split on, whereas the other boundaries do not. /// /// `Boundary` includes methods that return useful groups of boundaries. It also /// contains the [`defaults_from`](Boundary::defaults_from) method which will generate a subset /// of default boundaries based on the boundaries present in a string. /// /// You can also create custom delimiter boundaries using the [`separator`](crate::separator) /// macro or directly instantiate `Boundary` for complex boundary conditions. /// ``` /// use convert_case::{Boundary, Case, Casing, Converter}; /// /// assert_eq!( /// "TransformationsIn3D" /// .from_case(Case::Camel) /// .remove_boundaries(&Boundary::digit_letter()) /// .to_case(Case::Snake), /// "transformations_in_3d", /// ); /// /// let conv = Converter::new() /// .set_boundaries(&Boundary::defaults_from("aA ")) /// .to_case(Case::Title); /// assert_eq!(conv.convert("myVariable Name"), "My Variable Name"); /// ``` /// /// ## Example /// /// For more complex boundaries, such as splitting based on the first character being a certain /// symbol and the second is lowercase, you can instantiate a boundary directly. /// /// ``` /// # use convert_case::{Boundary, Case, Casing}; /// let at_then_letter = Boundary::Custom { /// condition: |s| { /// s.get(0).map(|c| *c == "@") == Some(true) /// && s.get(1).map(|c| *c == c.to_lowercase()) == Some(true) /// }, /// start: 1, /// len: 0, /// }; /// assert_eq!( /// "name@domain" /// .set_boundaries(&[at_then_letter]) /// .to_case(Case::Title), /// "Name@ Domain", /// ) /// ``` #[derive(Debug, Clone, Copy)] pub enum Boundary { Custom { /// A function that determines if this boundary is present at the start /// of the string. Second argument is the `arg` field. condition: fn(&[&str]) -> bool, /// Where the beginning of the boundary is. start: usize, /// The length of the boundary. This is the number of graphemes that /// are removed when splitting. len: usize, }, /// Splits on `-`, consuming the character on segmentation. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::defaults_from("-"), /// vec![Boundary::Hyphen], /// ); /// ``` Hyphen, /// Splits on `_`, consuming the character on segmentation. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::defaults_from("_"), /// vec![Boundary::Underscore], /// ); /// ``` Underscore, /// Splits on space, consuming the character on segmentation. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::defaults_from(" "), /// vec![Boundary::Space], /// ); /// ``` Space, /// Splits where an uppercase letter is followed by a lowercase letter. This is seldom used, /// and is **not** included in the [defaults](Boundary::defaults). /// ``` /// # use convert_case::Boundary; /// assert!(Boundary::defaults_from("Aa").is_empty()); UpperLower, /// Splits where a lowercase letter is followed by an uppercase letter. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::defaults_from("aA"), /// vec![Boundary::LowerUpper], /// ); /// ``` LowerUpper, /// Splits where digit is followed by an uppercase letter. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::defaults_from("1A"), /// vec![Boundary::DigitUpper], /// ); /// ``` DigitUpper, /// Splits where an uppercase letter is followed by a digit. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::defaults_from("A1"), /// vec![Boundary::UpperDigit], /// ); /// ``` UpperDigit, /// Splits where digit is followed by a lowercase letter. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::defaults_from("1a"), /// vec![Boundary::DigitLower], /// ); /// ``` DigitLower, /// Splits where a lowercase letter is followed by a digit. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::defaults_from("a1"), /// vec![Boundary::LowerDigit], /// ); /// ``` LowerDigit, /// Acronyms are identified by two uppercase letters followed by a lowercase letter. /// The word boundary is between the two uppercase letters. For example, "HTTPRequest" /// would have an acronym boundary identified at "PRe" and split into "HTTP" and "Request". /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::defaults_from("AAa"), /// vec![Boundary::Acronym], /// ); /// ``` Acronym, } impl Boundary { pub fn matches(self, s: &[&str]) -> bool { use Boundary::*; match self { Underscore => s.first() == Some(&"_"), Hyphen => s.first() == Some(&"-"), Space => s.first() == Some(&" "), Acronym => { s.first().map(grapheme_is_uppercase) == Some(true) && s.get(1).map(grapheme_is_uppercase) == Some(true) && s.get(2).map(grapheme_is_lowercase) == Some(true) } LowerUpper => { s.first().map(grapheme_is_lowercase) == Some(true) && s.get(1).map(grapheme_is_uppercase) == Some(true) } UpperLower => { s.first().map(grapheme_is_uppercase) == Some(true) && s.get(1).map(grapheme_is_lowercase) == Some(true) } LowerDigit => { s.first().map(grapheme_is_lowercase) == Some(true) && s.get(1).map(grapheme_is_digit) == Some(true) } UpperDigit => { s.first().map(grapheme_is_uppercase) == Some(true) && s.get(1).map(grapheme_is_digit) == Some(true) } DigitLower => { s.first().map(grapheme_is_digit) == Some(true) && s.get(1).map(grapheme_is_lowercase) == Some(true) } DigitUpper => { s.first().map(grapheme_is_digit) == Some(true) && s.get(1).map(grapheme_is_uppercase) == Some(true) } Custom { condition, .. } => condition(s), } } /// The number of graphemes consumed when splitting at the boundary. pub fn len(self) -> usize { use Boundary::*; match self { Underscore | Hyphen | Space => 1, LowerUpper | UpperLower | LowerDigit | UpperDigit | DigitLower | DigitUpper | Acronym => 0, Custom { len, .. } => len, } } /// Returns true if this boundary consumes no graphemes when splitting. pub fn is_empty(self) -> bool { self.len() == 0 } /// The index of the character to split at. pub fn start(self) -> usize { use Boundary::*; match self { Underscore | Hyphen | Space => 0, LowerUpper | UpperLower | LowerDigit | UpperDigit | DigitLower | DigitUpper | Acronym => 1, Custom { start, .. } => start, } } /// The default list of boundaries used when `Casing::to_case` is called directly /// and in a `Converter` generated from `Converter::new()`. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::defaults(), /// [ /// Boundary::Underscore, /// Boundary::Hyphen, /// Boundary::Space, /// Boundary::LowerUpper, /// Boundary::LowerDigit, /// Boundary::UpperDigit, /// Boundary::DigitLower, /// Boundary::DigitUpper, /// Boundary::Acronym, /// ], /// ); /// ``` pub const fn defaults() -> [Boundary; 9] { [ Boundary::Underscore, Boundary::Hyphen, Boundary::Space, Boundary::LowerUpper, Boundary::LowerDigit, Boundary::UpperDigit, Boundary::DigitLower, Boundary::DigitUpper, Boundary::Acronym, ] } /// Returns the boundaries that involve digits. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::digits(), /// [ /// Boundary::LowerDigit, /// Boundary::UpperDigit, /// Boundary::DigitLower, /// Boundary::DigitUpper, /// ], /// ); /// ``` pub const fn digits() -> [Boundary; 4] { [ Boundary::LowerDigit, Boundary::UpperDigit, Boundary::DigitLower, Boundary::DigitUpper, ] } /// Returns the boundaries that are letters followed by digits. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::letter_digit(), /// [ /// Boundary::LowerDigit, /// Boundary::UpperDigit, /// ], /// ); /// ``` pub const fn letter_digit() -> [Boundary; 2] { [Boundary::LowerDigit, Boundary::UpperDigit] } /// Returns the boundaries that are digits followed by letters. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::digit_letter(), /// [ /// Boundary::DigitLower, /// Boundary::DigitUpper /// ], /// ); /// ``` pub const fn digit_letter() -> [Boundary; 2] { [Boundary::DigitLower, Boundary::DigitUpper] } /// Returns a list of all boundaries that are identified within the given string. /// Could be a short of writing out all the boundaries in a list directly. This will not /// identify boundary `UpperLower` if it also used as part of `Acronym`. /// /// If you want to be very explicit and not overlap boundaries, it is recommended to use a colon /// character. /// ``` /// # use convert_case::Boundary; /// assert_eq!( /// Boundary::defaults_from("aA8a -"), /// vec![ /// Boundary::Hyphen, /// Boundary::Space, /// Boundary::LowerUpper, /// Boundary::UpperDigit, /// Boundary::DigitLower, /// ], /// ); /// assert_eq!( /// Boundary::defaults_from("bD:0B:_:AAa"), /// vec![ /// Boundary::Underscore, /// Boundary::LowerUpper, /// Boundary::DigitUpper, /// Boundary::Acronym, /// ], /// ); /// ``` pub fn defaults_from(pattern: &str) -> Vec { let mut boundaries = Vec::new(); for boundary in Boundary::defaults() { let parts = split(&pattern, &[boundary]); if parts.len() > 1 || parts.is_empty() || parts[0] != pattern { boundaries.push(boundary); } } boundaries } } impl PartialEq for Boundary { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Hyphen, Self::Hyphen) => true, (Self::Underscore, Self::Underscore) => true, (Self::Space, Self::Space) => true, (Self::UpperLower, Self::UpperLower) => true, (Self::LowerUpper, Self::LowerUpper) => true, (Self::DigitUpper, Self::DigitUpper) => true, (Self::UpperDigit, Self::UpperDigit) => true, (Self::DigitLower, Self::DigitLower) => true, (Self::LowerDigit, Self::LowerDigit) => true, (Self::Acronym, Self::Acronym) => true, // Custom boundaries are never equal because they contain function pointers, // which cannot be reliably compared. (Self::Custom { .. }, Self::Custom { .. }) => false, _ => false, } } } impl Eq for Boundary {} impl core::hash::Hash for Boundary { fn hash(&self, state: &mut H) { // Hash only the discriminant. Custom variants can't be meaningfully // compared or hashed by their function pointer, so all Custom variants // hash to the same value (their discriminant). core::mem::discriminant(self).hash(state); } } /// Split an identifier into a list of words using the list of boundaries. /// /// This is used internally for splitting an identifier before mutating by /// a pattern and joining again with a delimiter. /// ``` /// use convert_case::{Boundary, split}; /// assert_eq!( /// split(&"one_two-three.four", &[Boundary::Underscore, Boundary::Hyphen]), /// vec!["one", "two", "three.four"], /// ) /// ``` pub fn split<'s, T>(s: &'s T, boundaries: &[Boundary]) -> Vec<&'s str> where T: AsRef, { let s = s.as_ref(); if s.is_empty() { return Vec::new(); } let mut words = Vec::new(); let mut last_boundary_end = 0; let (indices, graphemes): (Vec<_>, Vec<_>) = s.grapheme_indices(true).unzip(); let grapheme_length = indices[graphemes.len() - 1] + graphemes[graphemes.len() - 1].len(); // TODO: // swapping the order of these would be faster // end the loop sooner if any boundary condition is met // could also hit a bitvector and do the splitting at the end? May or may not be faster for i in 0..graphemes.len() { for boundary in boundaries { //let byte_index = indices[i]; if boundary.matches(&graphemes[i..]) { // What if we find a condition at the end of the array? // Maybe we can stop early based on length // To do this, need to switch the loops // TODO let boundary_byte_start: usize = *indices .get(i + boundary.start()) .unwrap_or(&grapheme_length); let boundary_byte_end: usize = *indices .get(i + boundary.start() + boundary.len()) .unwrap_or(&grapheme_length); // todo clean this up a bit words.push(&s[last_boundary_end..boundary_byte_start]); last_boundary_end = boundary_byte_end; break; } } } words.push(&s[last_boundary_end..]); //words.into_iter().filter(|s| !s.is_empty()).collect() words.into_iter().collect() } /// Create a new boundary based on a string. /// /// This is shorthand for creating a boundary that splits on a specific string, and /// omits that string from the list of words. For more information, see [`Boundary`]. /// ``` /// # use convert_case::{Case, Converter, separator}; /// let conv = Converter::new() /// .set_boundaries(&[separator!("::")]) /// .to_case(Case::Camel); /// /// assert_eq!( /// conv.convert("my::var::name"), /// "myVarName", /// ) /// ``` #[macro_export] macro_rules! separator { ($delim:expr) => { convert_case::Boundary::Custom { condition: |s| s.join("").starts_with($delim), start: 0, len: $delim.len(), } }; } #[cfg(test)] mod tests { use super::*; use rstest::rstest; #[test] fn custom_boundary_inequality() { // Custom boundaries are never equal because they contain function pointers let a = Boundary::Custom { condition: |_| true, start: 0, len: 0, }; let b = a; assert_ne!(a, b) } #[test] fn default_boundary_equality() { assert_eq!(Boundary::Hyphen, Boundary::Hyphen); assert_eq!(Boundary::Space, Boundary::Space); assert_ne!(Boundary::Hyphen, Boundary::Space); } #[rstest] #[case(Boundary::Hyphen, "a-b-c", vec!["a", "b", "c"])] #[case(Boundary::Underscore, "a_b_c", vec!["a", "b", "c"])] #[case(Boundary::Space, "a b c", vec!["a", "b", "c"])] #[case(Boundary::LowerUpper, "lowerUpperUpper", vec!["lower", "Upper", "Upper"])] #[case(Boundary::UpperLower, "ABc", vec!["AB", "c"])] #[case(Boundary::Acronym, "XMLRequest", vec!["XML", "Request"])] #[case(Boundary::LowerDigit, "abc123", vec!["abc", "123"])] #[case(Boundary::UpperDigit, "ABC123", vec!["ABC", "123"])] #[case(Boundary::DigitLower, "123abc", vec!["123", "abc"])] #[case(Boundary::DigitUpper, "123ABC", vec!["123", "ABC"])] fn split_on_boundary( #[case] boundary: Boundary, #[case] input: &str, #[case] expected: Vec<&str>, ) { assert_eq!(split(&input, &[boundary]), expected); } #[test] fn split_on_multiple_delimiters() { let s = "aaa-bbb_ccc ddd ddd-eee"; let v = split( &s, &[Boundary::Space, Boundary::Underscore, Boundary::Hyphen], ); assert_eq!(v, vec!["aaa", "bbb", "ccc", "ddd", "ddd", "eee"]); } #[test] fn boundaries_found_in_string() { // upper lower is no longer a default assert_eq!(Boundary::defaults_from(".Aaaa"), Vec::::new()); assert_eq!( Boundary::defaults_from("a8.Aa.aA"), vec![Boundary::LowerUpper, Boundary::LowerDigit] ); assert_eq!( Boundary::defaults_from("b1B1b"), Boundary::digits().to_vec() ); assert_eq!( Boundary::defaults_from("AAa -_"), vec![ Boundary::Underscore, Boundary::Hyphen, Boundary::Space, Boundary::Acronym, ] ); } } convert_case-0.11.0/src/case.rs000064400000000000000000000370111046102023000144300ustar 00000000000000use crate::boundary::{self, Boundary}; use crate::pattern::Pattern; use alloc::string::String; use alloc::vec::Vec; /// Defines the case of an identifier. /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(title, "super_mario_64"), "Super Mario 64"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("super_mario_64".to_case(Case::Title), "Super Mario 64"); /// ``` /// /// A case is the pair of a [pattern](Pattern) and a delimiter (a string). Given /// a list of words, a pattern describes how to mutate the words and a delimiter is how the mutated /// words are joined together. /// /// | pattern | underscore `_` | hyphen `-` | empty string | space | /// | ---: | --- | --- | --- | --- | /// | [lowercase](Pattern::Lowercase) | [snake_case](Case::Snake) | [kebab-case](Case::Kebab) | [flatcase](Case::Flat) | [lower case](Case::Lower) | /// | [uppercase](Pattern::Uppercase) | [CONSTANT_CASE](Case::Constant) | [COBOL-CASE](Case::Cobol) | [UPPERFLATCASE](Case::UpperFlat) | [UPPER CASE](Case::Upper) | /// | [capital](Pattern::Capital) | [Ada_Case](Case::Ada) | [Train-Case](Case::Train) | [PascalCase](Case::Pascal) | [Title Case](Case::Title) | /// | [camel](Pattern::Camel) | | | [camelCase](Case::Camel) | /// /// There are additionally [`Case::Sentence`]. /// /// This crate provides the ability to convert "from" a case. This introduces a different feature /// of cases which are the [word boundaries](Boundary) that segment the identifier into words. For example, a /// snake case identifier `my_var_name` can be split on underscores `_` to segment into words. A /// camel case identifier `myVarName` is split where a lowercase letter is followed by an /// uppercase letter. Each case is also associated with a list of boundaries that are used when /// converting "from" a particular case. #[derive(Eq, PartialEq, Hash, Clone, Copy, Debug)] #[non_exhaustive] pub enum Case<'b> { /// Custom cases can be delimited by any static string slice and mutate words /// using any pattern. Further, they can use any list of boundaries for /// splitting identifiers into words. /// /// This flexibility can create cases not present as another variant of the /// Case enum. For instance, you could create a "dot case" like so. /// ``` /// use convert_case::{Case, Casing, separator, Pattern}; /// let dot_case = Case::Custom { /// boundaries: &[separator!(".")], /// pattern: Pattern::Lowercase, /// delimiter: ".", /// }; /// /// assert_eq!( /// "myNewCase".to_case(dot_case), /// "my.new.case", /// ); /// assert_eq!( /// "my.new.case".from_case(dot_case).to_case(Case::Title), /// "My New Case", /// ); /// ``` Custom { boundaries: &'b [Boundary], pattern: Pattern, delimiter: &'static str, }, /// Snake case strings are delimited by underscores `_` and are all lowercase. /// /// * Boundaries : [Underscore](Boundary::Underscore) /// * Pattern : [Lowercase](Pattern::Lowercase) /// * Delimiter : Underscore `"_"` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(snake, "My variable NAME"), "my_variable_name"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Snake), "my_variable_name"); /// ``` Snake, /// Constant case strings are delimited by underscores `_` and are all uppercase. /// * Boundaries: [Underscore](Boundary::Underscore) /// * Pattern: [Uppercase](Pattern::Uppercase) /// * Delimiter: Underscore `"_"` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(constant, "My variable NAME"), "MY_VARIABLE_NAME"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Constant), "MY_VARIABLE_NAME"); /// ``` Constant, /// Upper snake case is an alternative name for [constant case](Case::Constant). UpperSnake, /// Ada case strings are delimited by underscores `_`. The leading letter of /// each word is uppercase, while the rest is lowercase. /// * Boundaries: [Underscore](Boundary::Underscore) /// * Pattern: [Capital](Pattern::Capital) /// * Delimiter: Underscore `"_"` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(ada, "My variable NAME"), "My_Variable_Name"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Ada), "My_Variable_Name"); /// ``` Ada, /// Kebab case strings are delimited by hyphens `-` and are all lowercase. /// * Boundaries: [Hyphen](Boundary::Hyphen) /// * Pattern: [Lowercase](Pattern::Lowercase) /// * Delimiter: Hyphen `"-"` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(kebab, "My variable NAME"), "my-variable-name"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Kebab), "my-variable-name"); /// ``` Kebab, /// Cobol case strings are delimited by hyphens `-` and are all uppercase. /// * Boundaries: [Hyphen](Boundary::Hyphen) /// * Pattern: [Uppercase](Pattern::Uppercase) /// * Delimiter: Hyphen `"-"` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(cobol, "My variable NAME"), "MY-VARIABLE-NAME"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Cobol), "MY-VARIABLE-NAME"); /// ``` Cobol, /// Upper kebab case is an alternative name for [Cobol case](Case::Cobol). UpperKebab, /// Train case strings are delimited by hyphens `-`. The leading letter of /// each word is uppercase, while the rest is lowercase. /// * Boundaries: [Hyphen](Boundary::Hyphen) /// * Pattern: [Capital](Pattern::Capital) /// * Delimiter: Hyphen `"-"` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(train, "My variable NAME"), "My-Variable-Name"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Train), "My-Variable-Name"); /// ``` Train, /// Flat case strings are all lowercase, with no delimiter. Note that word boundaries are lost. /// * Boundaries: No boundaries /// * Pattern: [Lowercase](Pattern::Lowercase) /// * Delimiter: Empty string `""` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(flat, "My variable NAME"), "myvariablename"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Flat), "myvariablename"); /// ``` Flat, /// Upper flat case strings are all uppercase, with no delimiter. Note that word boundaries are lost. /// * Boundaries: No boundaries /// * Pattern: [Uppercase](Pattern::Uppercase) /// * Delimiter: Empty string `""` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(upper_flat, "My variable NAME"), "MYVARIABLENAME"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::UpperFlat), "MYVARIABLENAME"); /// ``` UpperFlat, /// Pascal case strings are lowercase, but for every word the /// first letter is capitalized. /// * Boundaries: [LowerUpper](Boundary::LowerUpper), [DigitUpper](Boundary::DigitUpper), /// [UpperDigit](Boundary::UpperDigit), [DigitLower](Boundary::DigitLower), /// [LowerDigit](Boundary::LowerDigit), [Acronym](Boundary::Acronym) /// * Pattern: [Capital](`Pattern::Capital`) /// * Delimiter: Empty string `""` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(pascal, "My variable NAME"), "MyVariableName"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Pascal), "MyVariableName"); /// ``` Pascal, /// Upper camel case is an alternative name for [Pascal case](Case::Pascal). UpperCamel, /// Camel case strings are lowercase, but for every word _except the first_ the /// first letter is capitalized. /// * Boundaries: [LowerUpper](Boundary::LowerUpper), [DigitUpper](Boundary::DigitUpper), /// [UpperDigit](Boundary::UpperDigit), [DigitLower](Boundary::DigitLower), /// [LowerDigit](Boundary::LowerDigit), [Acronym](Boundary::Acronym) /// * Pattern: [Camel](`Pattern::Camel`) /// * Delimiter: Empty string `""` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(camel, "My variable NAME"), "myVariableName"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Camel), "myVariableName"); /// ``` Camel, /// Lowercase strings are delimited by spaces and all characters are lowercase. /// * Boundaries: [Space](`Boundary::Space`) /// * Pattern: [Lowercase](`Pattern::Lowercase`) /// * Delimiter: Space `" "` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(lower, "My variable NAME"), "my variable name"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Lower), "my variable name"); /// ``` Lower, /// Uppercase strings are delimited by spaces and all characters are uppercase. /// * Boundaries: [Space](`Boundary::Space`) /// * Pattern: [Uppercase](`Pattern::Uppercase`) /// * Delimiter: Space `" "` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(upper, "My variable NAME"), "MY VARIABLE NAME"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Upper), "MY VARIABLE NAME"); /// ``` Upper, /// Title case strings are delimited by spaces. Only the leading character of /// each word is uppercase. No inferences are made about language, so words /// like "as", "to", and "for" will still be capitalized. /// * Boundaries: [Space](`Boundary::Space`) /// * Pattern: [Capital](`Pattern::Capital`) /// * Delimiter: Space `" "` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(title, "My variable NAME"), "My Variable Name"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Title), "My Variable Name"); /// ``` Title, /// Sentence case strings are delimited by spaces. Only the leading character of /// the first word is uppercase. /// * Boundaries: [Space](`Boundary::Space`) /// * Pattern: [Sentence](`Pattern::Sentence`) /// * Delimiter: Space `" "` /// /// ``` /// use convert_case::ccase; /// assert_eq!(ccase!(sentence, "My variable NAME"), "My variable name"); /// /// use convert_case::{Case, Casing}; /// assert_eq!("My variable NAME".to_case(Case::Sentence), "My variable name"); /// ``` Sentence, } impl Case<'_> { /// Returns the boundaries used in the corresponding case. That is, where can word boundaries /// be distinguished in a string of the given case. The table outlines which cases use which /// set of boundaries. /// /// | Cases | Boundaries | /// | --- | --- | /// | Snake, Constant, UpperSnake, Ada | [Underscore](Boundary::Underscore) | /// | Kebab, Cobol, UpperKebab, Train | [Hyphen](Boundary::Hyphen) | /// | Lower, Upper, Title | [Space](Boundary::Space) | /// | Pascal, UpperCamel, Camel | [LowerUpper](Boundary::LowerUpper), [LowerDigit](Boundary::LowerDigit), [UpperDigit](Boundary::UpperDigit), [DigitLower](Boundary::DigitLower), [DigitUpper](Boundary::DigitUpper), [Acronym](Boundary::Acronym) | /// | Flat, UpperFlat | No boundaries | pub fn boundaries(&self) -> &[Boundary] { use Case::*; match self { Snake | Constant | UpperSnake | Ada => &[Boundary::Underscore], Kebab | Cobol | UpperKebab | Train => &[Boundary::Hyphen], Upper | Lower | Title | Sentence => &[Boundary::Space], Camel | UpperCamel | Pascal => &[ Boundary::LowerUpper, Boundary::Acronym, Boundary::LowerDigit, Boundary::UpperDigit, Boundary::DigitLower, Boundary::DigitUpper, ], UpperFlat | Flat => &[], Custom { boundaries, .. } => boundaries, } } /// Returns the delimiter used in the corresponding case. The following /// table outlines which cases use which delimiter. /// /// | Cases | Delimiter | /// | --- | --- | /// | Snake, Constant, UpperSnake, Ada | Underscore `"_"` | /// | Kebab, Cobol, UpperKebab, Train | Hyphen `"-"` | /// | Upper, Lower, Title, Sentence | Space `" "` | /// | Flat, UpperFlat, Pascal, UpperCamel, Camel | Empty string `""` | pub const fn delimiter(&self) -> &'static str { use Case::*; match self { Snake | Constant | UpperSnake | Ada => "_", Kebab | Cobol | UpperKebab | Train => "-", Upper | Lower | Title | Sentence => " ", Flat | UpperFlat | Pascal | UpperCamel | Camel => "", Custom { delimiter: delim, .. } => delim, } } /// Returns the pattern used in the corresponding case. The following /// table outlines which cases use which pattern. /// /// | Cases | Pattern | /// | --- | --- | /// | Constant, UpperSnake, Cobol, UpperKebab, UpperFlat, Upper | [Uppercase](Pattern::Uppercase) | /// | Snake, Kebab, Flat, Lower | [Lowercase](Pattern::Lowercase) | /// | Ada, Train, Pascal, UpperCamel, Title | [Capital](Pattern::Capital) | /// | Camel | [Camel](Pattern::Camel) | pub const fn pattern(&self) -> Pattern { use Case::*; match self { Constant | UpperSnake | Cobol | UpperKebab | UpperFlat | Upper => Pattern::Uppercase, Snake | Kebab | Flat | Lower => Pattern::Lowercase, Ada | Train | Pascal | UpperCamel | Title => Pattern::Capital, Camel => Pattern::Camel, Sentence => Pattern::Sentence, Custom { pattern, .. } => *pattern, } } /// Split an identifier into words based on the boundaries of this case. /// ``` /// use convert_case::Case; /// assert_eq!( /// Case::Pascal.split(&"getTotalLength"), /// vec!["get", "Total", "Length"], /// ); /// ``` pub fn split(self, s: &T) -> Vec<&str> where T: AsRef, { boundary::split(s, self.boundaries()) } /// Mutate a list of words based on the pattern of this case. /// ``` /// use convert_case::Case; /// assert_eq!( /// Case::Snake.mutate(&["get", "Total", "Length"]), /// vec!["get", "total", "length"], /// ); /// ``` pub fn mutate(self, words: &[&str]) -> Vec { self.pattern().mutate(words) } /// Join a list of words into a single identifier using the delimiter of this case. /// ``` /// use convert_case::Case; /// assert_eq!( /// Case::Snake.join(&[ /// String::from("get"), /// String::from("total"), /// String::from("length") /// ]), /// String::from("get_total_length"), /// ); /// ``` pub fn join(self, words: &[String]) -> String { words.join(self.delimiter()) } /// Array of all non-custom case enum variants. Does not include aliases. pub fn all_cases() -> &'static [Case<'static>] { use Case::*; &[ Snake, Constant, Ada, Kebab, Cobol, Train, Flat, UpperFlat, Pascal, Camel, Upper, Lower, Title, Sentence, ] } } convert_case-0.11.0/src/converter.rs000064400000000000000000000346021046102023000155270ustar 00000000000000use crate::boundary; use crate::boundary::Boundary; use crate::pattern::Pattern; use crate::Case; use alloc::string::{String, ToString}; use alloc::vec; use alloc::vec::Vec; /// The parameters for performing a case conversion. /// /// A `Converter` stores three fields needed for case conversion. /// 1) `boundaries`: how a string is split into _words_. /// 2) `patterns`: how words are mutated, or how each character's case will change. /// 3) `delimiter`: how the mutated words are joined into the final string. /// /// Then calling [`convert`](Converter::convert) on a `Converter` will apply a case conversion /// defined by those fields. The `Converter` struct is what is used underneath those functions /// available in the `Casing` struct. /// /// You can use `Converter` when you need more specificity on conversion /// than those provided in `Casing`, or if it is simply more convenient or explicit. /// /// ``` /// use convert_case::{Boundary, Case, Casing, Converter, Pattern}; /// /// let s = "DialogueBox-border-shadow"; /// /// // Convert using Casing trait /// assert_eq!( /// s.from_case(Case::Kebab).to_case(Case::Snake), /// "dialoguebox_border_shadow", /// ); /// /// // Convert using similar methods on Converter /// let conv = Converter::new() /// .from_case(Case::Kebab) /// .to_case(Case::Snake); /// assert_eq!(conv.convert(s), "dialoguebox_border_shadow"); /// /// // Convert by setting each field explicitly /// let conv = Converter::new() /// .set_boundaries(&[Boundary::Hyphen]) /// .set_patterns(&[Pattern::Lowercase]) /// .set_delimiter("_"); /// assert_eq!(conv.convert(s), "dialoguebox_border_shadow"); /// ``` /// /// Or you can use `Converter` when you are performing a transformation /// not provided as a variant of `Case`. /// /// ``` /// # use convert_case::{Boundary, Case, Casing, Converter, Pattern}; /// let dot_camel = Converter::new() /// .set_boundaries(&[Boundary::LowerUpper, Boundary::LowerDigit]) /// .set_patterns(&[Pattern::Camel]) /// .set_delimiter("."); /// assert_eq!(dot_camel.convert("CollisionShape2D"), "collision.Shape.2d"); /// ``` pub struct Converter { /// How a string is split into words. pub boundaries: Vec, /// How each word is mutated before joining. pub patterns: Vec, /// The string used to join mutated words together. pub delimiter: String, } impl Default for Converter { fn default() -> Self { Converter { boundaries: Boundary::defaults().to_vec(), patterns: Vec::new(), delimiter: String::new(), } } } impl Converter { /// Creates a new `Converter` with default fields. This is the same as `Default::default()`. /// The `Converter` will use [`Boundary::defaults()`] for boundaries, no patterns, and an empty /// string as a delimiter. /// ``` /// # use convert_case::Converter; /// let conv = Converter::new(); /// assert_eq!(conv.convert("Ice-cream TRUCK"), "IcecreamTRUCK") /// ``` pub fn new() -> Self { Self::default() } /// Converts a string. /// ``` /// # use convert_case::{Case, Converter}; /// let conv = Converter::new() /// .to_case(Case::Camel); /// assert_eq!(conv.convert("XML_HTTP_Request"), "xmlHttpRequest") /// ``` pub fn convert(&self, s: T) -> String where T: AsRef, { let words = boundary::split(&s, &self.boundaries); let mut result: Vec = words.into_iter().map(|s| s.to_string()).collect(); for pattern in &self.patterns { result = pattern.mutate(&result); } result.join(&self.delimiter) } /// Set the pattern and delimiter to those associated with the given case. /// ``` /// # use convert_case::{Case, Converter}; /// let conv = Converter::new() /// .to_case(Case::Pascal); /// assert_eq!(conv.convert("variable name"), "VariableName") /// ``` pub fn to_case(mut self, case: Case) -> Self { self.patterns.push(case.pattern()); self.delimiter = case.delimiter().to_string(); self } /// Sets the boundaries to those associated with the provided case. This is used /// by the `from_case` function in the `Casing` trait. /// ``` /// # use convert_case::{Case, Converter}; /// let conv = Converter::new() /// .from_case(Case::Snake) /// .to_case(Case::Title); /// assert_eq!(conv.convert("dot_productValue"), "Dot Productvalue") /// ``` pub fn from_case(mut self, case: Case) -> Self { self.boundaries = case.boundaries().to_vec(); self } /// Sets the boundaries to those provided. /// ``` /// # use convert_case::{Boundary, Case, Converter}; /// let conv = Converter::new() /// .set_boundaries(&[Boundary::Underscore, Boundary::LowerUpper]) /// .to_case(Case::Lower); /// assert_eq!(conv.convert("firstName_lastName"), "first name last name"); /// ``` pub fn set_boundaries(mut self, bs: &[Boundary]) -> Self { self.boundaries = bs.to_vec(); self } /// Adds a boundary to the list of boundaries. /// ``` /// # use convert_case::{Boundary, Case, Converter}; /// let conv = Converter::new() /// .from_case(Case::Title) /// .add_boundary(Boundary::Hyphen) /// .to_case(Case::Snake); /// assert_eq!(conv.convert("My Biography - Video 1"), "my_biography___video_1") /// ``` pub fn add_boundary(mut self, b: Boundary) -> Self { self.boundaries.push(b); self } /// Adds a vector of boundaries to the list of boundaries. /// ``` /// # use convert_case::{Boundary, Case, Converter}; /// let conv = Converter::new() /// .from_case(Case::Kebab) /// .to_case(Case::Title) /// .add_boundaries(&[Boundary::Underscore, Boundary::LowerUpper]); /// assert_eq!(conv.convert("2020-10_firstDay"), "2020 10 First Day"); /// ``` pub fn add_boundaries(mut self, bs: &[Boundary]) -> Self { self.boundaries.extend(bs); self } /// Removes a boundary from the list of boundaries if it exists. /// /// Note: [`Boundary::Custom`] variants are never considered equal due to /// function pointer comparison limitations, so they cannot be removed using this method. /// Recall that the default boundaries include no custom enumerations. /// ``` /// # use convert_case::{Boundary, Case, Converter}; /// let conv = Converter::new() /// .remove_boundary(Boundary::Acronym) /// .to_case(Case::Kebab); /// assert_eq!(conv.convert("HTTPRequest_parser"), "httprequest-parser"); /// ``` pub fn remove_boundary(mut self, b: Boundary) -> Self { self.boundaries.retain(|&x| x != b); self } /// Removes all the provided boundaries from the list of boundaries if it exists. /// /// Note: [`Boundary::Custom`] variants are never considered equal due to /// function pointer comparison limitations, so they cannot be removed using this method. /// Recall that the default boundaries include no custom enumerations. /// ``` /// # use convert_case::{Boundary, Case, Converter}; /// let conv = Converter::new() /// .remove_boundaries(&Boundary::digits()) /// .to_case(Case::Snake); /// assert_eq!(conv.convert("C04 S03 Path Finding.pdf"), "c04_s03_path_finding.pdf"); /// ``` pub fn remove_boundaries(mut self, bs: &[Boundary]) -> Self { for b in bs { self.boundaries.retain(|&x| x != *b); } self } /// Sets a single pattern, replacing any existing patterns. /// ``` /// # use convert_case::{Converter, Pattern}; /// let conv = Converter::new() /// .set_delimiter("_") /// .set_pattern(Pattern::Sentence); /// assert_eq!(conv.convert("BJARNE CASE"), "Bjarne_case"); /// ``` pub fn set_pattern(mut self, p: Pattern) -> Self { self.patterns = vec![p]; self } /// Sets the patterns to those provided, replacing any existing patterns. /// An empty slice means no mutation (words pass through unchanged). /// ``` /// # use convert_case::{Converter, Pattern}; /// let conv = Converter::new() /// .set_delimiter("_") /// .set_patterns(&[Pattern::Sentence]); /// assert_eq!(conv.convert("BJARNE CASE"), "Bjarne_case"); /// ``` pub fn set_patterns(mut self, ps: &[Pattern]) -> Self { self.patterns = ps.to_vec(); self } /// Adds a pattern to the end of the pattern list. /// Patterns are applied in order, so this pattern will be applied last. /// ``` /// # use convert_case::{Case, Converter, Pattern}; /// let conv = Converter::new() /// .from_case(Case::Kebab) /// .add_pattern(Pattern::RemoveEmpty) /// .add_pattern(Pattern::Camel); /// assert_eq!(conv.convert("--leading-delims"), "leadingDelims"); /// ``` pub fn add_pattern(mut self, p: Pattern) -> Self { self.patterns.push(p); self } /// Adds multiple patterns to the end of the pattern list. /// ``` /// # use convert_case::{Converter, Pattern}; /// let conv = Converter::new() /// .add_patterns(&[Pattern::RemoveEmpty, Pattern::Lowercase]); /// ``` pub fn add_patterns(mut self, ps: &[Pattern]) -> Self { self.patterns.extend(ps); self } /// Removes a pattern from the list if it exists. /// /// Note: [`Pattern::Custom`] variants are never considered equal due to /// function pointer comparison limitations, so they cannot be removed using this method. /// ``` /// # use convert_case::{Boundary, Case, Converter, Pattern}; /// let conv = Converter::new() /// .set_boundaries(&[Boundary::Space]) /// .to_case(Case::Snake) /// .remove_pattern(Pattern::Lowercase); /// assert_eq!(conv.convert("HeLLo WoRLD"), "HeLLo_WoRLD"); /// ``` pub fn remove_pattern(mut self, p: Pattern) -> Self { self.patterns.retain(|&x| x != p); self } /// Removes all specified patterns from the list. /// /// Note: [`Pattern::Custom`] variants are never considered equal due to /// function pointer comparison limitations, so they cannot be removed using this method. /// ``` /// # use convert_case::{Converter, Pattern}; /// let conv = Converter::new() /// .set_patterns(&[Pattern::RemoveEmpty, Pattern::Lowercase, Pattern::Capital]) /// .remove_patterns(&[Pattern::Lowercase, Pattern::Capital]); /// // Only RemoveEmpty remains /// ``` pub fn remove_patterns(mut self, ps: &[Pattern]) -> Self { for p in ps { self.patterns.retain(|&x| x != *p); } self } /// Sets the delimiter. /// ``` /// # use convert_case::{Case, Converter}; /// let conv = Converter::new() /// .to_case(Case::Snake) /// .set_delimiter("."); /// assert_eq!(conv.convert("LowerWithDots"), "lower.with.dots"); /// ``` pub fn set_delimiter(mut self, d: T) -> Self where T: ToString, { self.delimiter = d.to_string(); self } } #[cfg(test)] mod test { use super::*; use crate::Casing; #[test] fn snake_converter_from_case() { let conv = Converter::new().to_case(Case::Snake); let s = String::from("my var name"); assert_eq!(s.to_case(Case::Snake), conv.convert(s)); } #[test] fn snake_converter_from_scratch() { let conv = Converter::new() .set_delimiter("_") .set_patterns(&[Pattern::Lowercase]); let s = String::from("my var name"); assert_eq!(s.to_case(Case::Snake), conv.convert(s)); } #[test] fn custom_pattern() { let conv = Converter::new() .to_case(Case::Snake) .set_patterns(&[Pattern::Sentence]); assert_eq!(conv.convert("bjarne case"), "Bjarne_case"); } #[test] fn custom_delim() { let conv = Converter::new().set_delimiter(".."); assert_eq!(conv.convert("ohMy"), "oh..My"); } #[test] fn no_delim() { let conv = Converter::new() .from_case(Case::Title) .to_case(Case::Kebab) .set_delimiter(""); assert_eq!(conv.convert("Just Flat"), "justflat"); } #[test] fn no_digit_boundaries() { let conv = Converter::new() .remove_boundaries(&Boundary::digits()) .to_case(Case::Snake); assert_eq!(conv.convert("Test 08Bound"), "test_08bound"); assert_eq!(conv.convert("a8aA8A"), "a8a_a8a"); } #[test] fn remove_boundary() { let conv = Converter::new() .remove_boundary(Boundary::DigitUpper) .to_case(Case::Snake); assert_eq!(conv.convert("Test 08Bound"), "test_08bound"); assert_eq!(conv.convert("a8aA8A"), "a_8_a_a_8a"); } #[test] fn add_boundary() { let conv = Converter::new() .from_case(Case::Snake) .to_case(Case::Kebab) .add_boundary(Boundary::LowerUpper); assert_eq!(conv.convert("word_wordWord"), "word-word-word"); } #[test] fn add_boundaries() { let conv = Converter::new() .from_case(Case::Snake) .to_case(Case::Kebab) .add_boundaries(&[Boundary::LowerUpper, Boundary::UpperLower]); assert_eq!(conv.convert("word_wordWord"), "word-word-w-ord"); } #[test] fn twice() { let s = "myVarName".to_string(); let conv = Converter::new().to_case(Case::Snake); let snake = conv.convert(&s); let kebab = s.to_case(Case::Kebab); assert_eq!(snake.to_case(Case::Camel), kebab.to_case(Case::Camel)); } #[test] fn reuse_after_change() { let conv = Converter::new().from_case(Case::Snake).to_case(Case::Kebab); assert_eq!(conv.convert("word_wordWord"), "word-wordword"); let conv = conv.add_boundary(Boundary::LowerUpper); assert_eq!(conv.convert("word_wordWord"), "word-word-word"); } #[test] fn explicit_boundaries() { let conv = Converter::new() .set_boundaries(&[ Boundary::DigitLower, Boundary::DigitUpper, Boundary::Acronym, ]) .to_case(Case::Snake); assert_eq!( conv.convert("section8lesson2HTTPRequests"), "section8_lesson2_http_requests" ); } } convert_case-0.11.0/src/lib.rs000064400000000000000000000634031046102023000142670ustar 00000000000000//! Convert to and from different string cases. //! //! # Basic Usage //! //! The most common use of this crate is to just convert a string into a //! particular case, like snake, camel, or kebab. You can use the [`ccase`] //! macro to convert most string types into the new case. //! ``` //! use convert_case::ccase; //! //! let s = "myVarName"; //! assert_eq!(ccase!(snake, s), "my_var_name"); //! assert_eq!(ccase!(kebab, s), "my-var-name"); //! assert_eq!(ccase!(pascal, s), "MyVarName"); //! assert_eq!(ccase!(title, s), "My Var Name"); //! ``` //! //! For more explicit conversion, import the [`Casing`] trait which adds methods //! to string types that perform the conversion based on a variant of the [`Case`] enum. //! ``` //! use convert_case::{Case, Casing}; //! //! let s = "myVarName"; //! assert_eq!(s.to_case(Case::Snake), "my_var_name"); //! assert_eq!(s.to_case(Case::Kebab), "my-var-name"); //! assert_eq!(s.to_case(Case::Pascal), "MyVarName"); //! assert_eq!(s.to_case(Case::Title), "My Var Name"); //! ``` //! //! For a full list of cases, see [`Case`]. //! //! # Splitting Conditions //! //! Case conversion starts by splitting a single identifier into a list of words. The //! condition for when to split and how to perform the split is defined by a [`Boundary`]. //! //! By default, [`ccase`] and [`Casing::to_case`] will split identifiers at all locations //! based on a list of [default boundaries](Boundary::defaults). //! //! ``` //! use convert_case::ccase; //! //! assert_eq!(ccase!(pascal, "hyphens-and_underscores"), "HyphensAndUnderscores"); //! assert_eq!(ccase!(pascal, "lowerUpper space"), "LowerUpperSpace"); //! assert_eq!(ccase!(snake, "HTTPRequest"), "http_request"); //! assert_eq!(ccase!(snake, "vector4d"), "vector_4_d") //! ``` //! //! Associated with each case is a [list of boundaries](Case::boundaries) that can be //! used to split identifiers instead of the defaults. We can use the following notation //! with the [`ccase`] macro. //! ``` //! use convert_case::ccase; //! //! assert_eq!( //! ccase!(title, "1999-25-01_family_photo.png"), //! "1999 25 01 Family Photo.png", //! ); //! assert_eq!( //! ccase!(snake -> title, "1999-25-01_family_photo.png"), //! "1999-25-01 Family Photo.png", //! ); //! ``` //! Or we can use the [`from_case`](Casing::from_case) method on `Casing` before calling //! `to_case`. //! ``` //! use convert_case::{Case, Casing}; //! //! assert_eq!( //! "John McCarthy".to_case(Case::Snake), //! "john_mc_carthy", //! ); //! assert_eq!( //! "John McCarthy".from_case(Case::Title).to_case(Case::Snake), //! "john_mccarthy", //! ); //! ``` //! You can remove boundaries from the list of defaults with [`Casing::remove_boundaries`]. See //! the list of constants on [`Boundary`] for splitting conditions. //! ``` //! use convert_case::{Boundary, Case, Casing}; //! //! assert_eq!( //! "Vector4D".remove_boundaries(&[Boundary::DigitUpper]).to_case(Case::Snake), //! "vector_4d", //! ); //! ``` //! //! # Other Behavior //! //! ### Acronyms //! Part of the default list of boundaries is [`acronym`](Boundary::Acronym) which //! will detect two capital letters followed by a lowercase letter. But there is no memory //! that the word itself was parsed considered an acronym. //! ``` //! # use convert_case::ccase; //! assert_eq!(ccase!(snake, "HTTPRequest"), "http_request"); //! assert_eq!(ccase!(pascal, "HTTPRequest"), "HttpRequest"); //! ``` //! //! ### Digits //! The default list of boundaries includes splitting before and after digits. //! ``` //! # use convert_case::ccase; //! assert_eq!(ccase!(title, "word2vec"), "Word 2 Vec"); //! ``` //! //! ### Unicode //! Conversion works on _graphemes_ as defined by the //! [`unicode_segmentation`](unicode_segmentation::UnicodeSegmentation::graphemes) library. //! This means that transforming letters to lowercase or uppercase works on all unicode //! characters, which also means that the number of characters isn't necessarily the //! same after conversion. //! ``` //! # use convert_case::ccase; //! assert_eq!(ccase!(kebab, "GranatÄpfel"), "granat-äpfel"); //! assert_eq!(ccase!(title, "ПЕРСПЕКТИВА24"), "Перспектива 24"); //! assert_eq!(ccase!(lower, "ὈΔΥΣΣΕΎΣ"), "ὀδυσσεύς"); //! ``` //! //! ### Symbols //! All symbols that are not part of the default boundary conditions are ignored. This //! is any symbol that isn't an underscore, hyphen, or space. //! ``` //! # use convert_case::ccase; //! assert_eq!(ccase!(snake, "dots.arent.default"), "dots.arent.default"); //! assert_eq!(ccase!(pascal, "path/to/file_name"), "Path/to/fileName"); //! assert_eq!(ccase!(pascal, "list\nof\nwords"), "List\nof\nwords"); //! ``` //! //! ### Delimiters //! Leading, trailing, and duplicate delimiters create empty words. //! This propagates and the converted string will share the behavior. **This can cause //! unintuitive behavior for patterns that transform words based on index.** //! ``` //! # use convert_case::ccase; //! assert_eq!(ccase!(constant, "_leading_score"), "_LEADING_SCORE"); //! assert_eq!(ccase!(ada, "trailing-dash-"), "Trailing_Dash_"); //! assert_eq!(ccase!(train, "duplicate----hyphens"), "Duplicate----Hyphens"); //! //! // not what you might expect! //! assert_eq!(ccase!(camel, "_empty__first_word"), "EmptyFirstWord"); //! ``` //! To remove empty words before joining, you can call `remove_empty` from the //! `Casing` trait before finishing the conversion. //! ``` //! # use convert_case::{Casing, Case}; //! assert_eq!( //! "_empty__first_word".remove_empty().to_case(Case::Camel), //! "emptyFirstWord" //! ) //! ``` //! //! # Customizing Behavior //! //! Case conversion takes place in three steps: //! 1. Splitting the identifier into a list of words //! 2. Mutating the letter case of graphemes within each word //! 3. Joining the words back into an identifier using a delimiter //! //! Those are defined by boundaries, patterns, and delimiters respectively. Graphically: //! //! ```md //! Identifier Identifier' //! | ^ //! | boundaries | delimiter //! V | //! Words ----------> Words' //! patterns //! ``` //! //! ## Patterns //! //! How to change the case of letters across a list of words is called a _pattern_. //! A pattern is a function that when passed a `&[&str]`, produces a //! `Vec`. The [`Pattern`] enum encapsulates the common transformations //! used across all cases. Although custom functions can be supplied with the //! [`Custom`](Pattern::Custom) variant. //! //! ## Boundaries //! //! The condition for splitting at part of an identifier, where to perform //! the split, and if any characters are removed are defined by [boundaries](Boundary). //! By default, identifiers are split based on [`Boundary::defaults`]. This list //! contains word boundaries that you would likely see after creating a multi-word //! identifier of typical cases. //! //! Custom boundary conditions can also be created. Commonly, you might split based on some //! character or list of characters. The [`separator`] macro builds //! a boundary that splits on the presence of a string, and then removes the string //! while producing the list of words. //! //! You can also use [`Boundary::Custom`] to explicitly define boundary //! conditions. If you actually need to create a //! boundary condition from scratch, you should file an issue to let the author know //! how you used it. I'm not certain what other boundary condition would be helpful. //! //! ## Cases //! //! A case is defined by a list of boundaries, a pattern, and a _delimiter_: the string to //! intersperse between words before concatenation. [`Case::Custom`] is a struct enum variant with //! exactly those three fields. You could create your own case like so. //! ``` //! use convert_case::{Case, Casing, separator, Pattern}; //! //! let dot_case = Case::Custom { //! boundaries: &[separator!(".")], //! pattern: Pattern::Lowercase, //! delimiter: ".", //! }; //! //! assert_eq!("AnimalFactoryFactory".to_case(dot_case), "animal.factory.factory"); //! //! assert_eq!( //! "pd.options.mode.copy_on_write" //! .from_case(dot_case) //! .to_case(Case::Title), //! "Pd Options Mode Copy_on_write", //! ) //! ``` //! //! ## Converter //! //! Case conversion with `convert_case` allows using attributes from two cases. From //! the first case is how you split the identifier (the _from_ case), and //! from the second is how to mutate and join the words (the _to_ case.) The //! [`Converter`] is used to define the _conversion_ process, not a case directly. //! //! It has the same fields as case, but is exposed via a builder interface //! and can be used to apply a conversion on a string directly, without //! specifying all the parameters at the time of conversion. //! //! In the below example, we build a converter that maps the double colon //! delimited module path in rust into a series of file directories. //! //! ``` //! use convert_case::{Case, Converter, separator}; //! //! let modules_into_path = Converter::new() //! .set_boundaries(&[separator!("::")]) //! .set_delimiter("/"); //! //! assert_eq!( //! modules_into_path.convert("std::os::unix"), //! "std/os/unix", //! ); //! ``` //! //! # Associated Projects //! //! ## Rust library `convert_case_extras` //! //! Some extra utilities for convert_case that don't need to be in the main library. //! You can read more here: [`convert_case_extras`](https://docs.rs/convert_case_extras). //! //! ## stringcase.org //! //! While developing `convert_case`, the author became fascinated in the naming conventions //! used for cases as well as different implementations for conversion. On [stringcase.org](https://stringcase.org) //! is documentation of the history of naming conventions, a catalogue of case conversion tools, //! and a more rigorous definition of what it means to "convert the case of an identifier." //! //! ## Command Line Utility `ccase` //! //! `convert_case` was originally developed for the purposes of a command line utility //! for converting the case of strings and filenames. You can check out //! [`ccase` on Github](https://github.com/rutrum/ccase). #![cfg_attr(not(test), no_std)] extern crate alloc; use alloc::string::String; mod boundary; mod case; mod converter; mod pattern; pub use boundary::{split, Boundary}; pub use case::Case; pub use converter::Converter; pub use pattern::Pattern; /// Describes items that can be converted into a case. This trait is used /// in conjunction with the [`StateConverter`] struct which is returned from a couple /// methods on `Casing`. pub trait Casing> { /// Convert the string into the given case. It will reference `self` and create a new /// `String` with the same pattern and delimiter as `case`. It will split on boundaries /// defined at [`Boundary::defaults()`]. /// ``` /// use convert_case::{Case, Casing}; /// /// assert_eq!( /// "Tetronimo piece border".to_case(Case::Kebab), /// "tetronimo-piece-border", /// ); /// ``` fn to_case(&self, case: Case) -> String; /// Start the case conversion by storing the boundaries associated with the given case. /// ``` /// use convert_case::{Case, Casing}; /// /// assert_eq!( /// "2020-08-10 Dannie Birthday" /// .from_case(Case::Title) /// .to_case(Case::Snake), /// "2020-08-10_dannie_birthday", /// ); /// ``` #[allow(clippy::wrong_self_convention)] fn from_case(&self, case: Case) -> StateConverter<'_, T>; /// Creates a `StateConverter` struct initialized with the boundaries provided. /// ``` /// use convert_case::{Boundary, Case, Casing}; /// /// assert_eq!( /// "E1M1 Hangar" /// .set_boundaries(&[Boundary::DigitUpper, Boundary::Space]) /// .to_case(Case::Snake), /// "e1_m1_hangar", /// ); /// ``` fn set_boundaries(&self, bs: &[Boundary]) -> StateConverter<'_, T>; /// Creates a `StateConverter` struct initialized without the boundaries /// provided. /// ``` /// use convert_case::{Boundary, Case, Casing}; /// /// assert_eq!( /// "2d_transformation", /// "2dTransformation" /// .remove_boundaries(&Boundary::digits()) /// .to_case(Case::Snake) /// ); /// ``` fn remove_boundaries(&self, bs: &[Boundary]) -> StateConverter<'_, T>; /// Creates a `StateConverter` with the `RemoveEmpty` pattern prepended. /// This filters out empty words before conversion, useful when splitting /// produces empty words from leading, trailing, and duplicate delimiters. /// ``` /// use convert_case::{Case, Casing}; /// /// assert_eq!( /// "--leading-delims" /// .from_case(Case::Kebab) /// .remove_empty() /// .to_case(Case::Camel), /// "leadingDelims", /// ); /// ``` fn remove_empty(&self) -> StateConverter<'_, T>; } impl> Casing for T { fn to_case(&self, case: Case) -> String { StateConverter::new(self).to_case(case) } fn set_boundaries(&self, bs: &[Boundary]) -> StateConverter<'_, T> { StateConverter::new(self).set_boundaries(bs) } fn remove_boundaries(&self, bs: &[Boundary]) -> StateConverter<'_, T> { StateConverter::new(self).remove_boundaries(bs) } fn from_case(&self, case: Case) -> StateConverter<'_, T> { StateConverter::new(self).from_case(case) } fn remove_empty(&self) -> StateConverter<'_, T> { StateConverter::new(self).remove_empty() } } /// Holds information about parsing before converting into a case. /// /// This struct is used when invoking the `from_case` and `with_boundaries` methods on /// `Casing`. For a more fine grained approach to case conversion, consider using the [`Converter`] /// struct. /// ``` /// # use convert_case::{Case, Casing}; /// assert_eq!( /// "By-Tor And The Snow Dog".from_case(Case::Title).to_case(Case::Snake), /// "by-tor_and_the_snow_dog", /// ); /// ``` pub struct StateConverter<'a, T: AsRef> { s: &'a T, conv: Converter, } impl<'a, T: AsRef> StateConverter<'a, T> { /// Only called by Casing function to_case() fn new(s: &'a T) -> Self { Self { s, conv: Converter::new(), } } /// Uses the boundaries associated with `case` for word segmentation. This /// will overwrite any boundary information initialized before. This method is /// likely not useful, but provided anyway. /// ``` /// use convert_case::{Case, Casing}; /// assert_eq!( /// "Alan Turing" /// .from_case(Case::Snake) // from Casing trait /// .from_case(Case::Title) // from StateConverter, overwrites previous /// .to_case(Case::Kebab), /// "alan-turing" /// ); /// ``` pub fn from_case(self, case: Case) -> Self { Self { conv: self.conv.from_case(case), ..self } } /// Overwrites boundaries for word segmentation with those provided. This will overwrite /// any boundary information initialized before. This method is likely not useful, but /// provided anyway. /// ``` /// use convert_case::{Boundary, Case, Casing}; /// assert_eq!( /// "Vector5d Transformation" /// .from_case(Case::Title) // from Casing trait /// .set_boundaries(&[Boundary::Space, Boundary::LowerDigit]) // overwrites `from_case` /// .to_case(Case::Kebab), /// "vector-5d-transformation" /// ); /// ``` pub fn set_boundaries(self, bs: &[Boundary]) -> Self { Self { s: self.s, conv: self.conv.set_boundaries(bs), } } /// Removes any boundaries that were already initialized. This is particularly useful when a /// case like `Case::Camel` has a lot of associated word boundaries, but you want to exclude /// some. /// ``` /// use convert_case::{Boundary, Case, Casing}; /// assert_eq!( /// "2dTransformation" /// .from_case(Case::Camel) /// .remove_boundaries(&Boundary::digits()) /// .to_case(Case::Snake), /// "2d_transformation" /// ); /// ``` pub fn remove_boundaries(self, bs: &[Boundary]) -> Self { Self { s: self.s, conv: self.conv.remove_boundaries(bs), } } /// Prepends the `RemoveEmpty` pattern to filter out empty words before conversion. /// This is useful when splitting produces empty words from leading, trailing, and /// duplicate delimiters. /// ``` /// use convert_case::{Case, Casing}; /// assert_eq!( /// "_leading_underscore" /// .from_case(Case::Snake) /// .remove_empty() /// .to_case(Case::Camel), /// "leadingUnderscore" /// ); /// ``` pub fn remove_empty(self) -> Self { Self { s: self.s, conv: self.conv.add_pattern(pattern::Pattern::RemoveEmpty), } } /// Consumes the `StateConverter` and returns the converted string. /// ``` /// use convert_case::{Boundary, Case, Casing}; /// assert_eq!( /// "Ice-Cream Social".from_case(Case::Title).to_case(Case::Lower), /// "ice-cream social", /// ); /// ``` pub fn to_case(self, case: Case) -> String { self.conv.to_case(case).convert(self.s) } } /// The variant of `case` from a token. /// /// The token associated with each variant is the variant written in snake case. /// To do conversion with a macro, see [`ccase`]. #[macro_export] macro_rules! case { (snake) => { convert_case::Case::Snake }; (constant) => { convert_case::Case::Constant }; (upper_snake) => { convert_case::Case::UpperSnake }; (ada) => { convert_case::Case::Ada; }; (kebab) => { convert_case::Case::Kebab }; (cobol) => { convert_case::Case::Cobol }; (upper_kebab) => { convert_case::Case::UpperKebab }; (train) => { convert_case::Case::Train }; (flat) => { convert_case::Case::Flat }; (upper_flat) => { convert_case::Case::UpperFlat }; (pascal) => { convert_case::Case::Pascal }; (upper_camel) => { convert_case::Case::UpperCamel }; (camel) => { convert_case::Case::Camel }; (lower) => { convert_case::Case::Lower }; (upper) => { convert_case::Case::Upper }; (title) => { convert_case::Case::Title }; (sentence) => { convert_case::Case::Sentence }; } /// Convert an identifier into a case. /// /// You can convert a string by writing the case name as a token. /// ``` /// use convert_case::ccase; /// /// assert_eq!(ccase!(snake, "myVarName"), "my_var_name"); /// // equivalent to /// // "myVarName".to_case(Case::Snake) /// ``` /// You can also specify a _from_ case, or the case that determines how the input /// string is split into words. /// ``` /// use convert_case::ccase; /// /// assert_eq!(ccase!(sentence -> snake, "Ice-cream sales"), "ice-cream_sales"); /// // equivalent to /// // "Ice-cream sales".from_case(Case::Sentence).to_case(Case::Snake) /// ``` #[macro_export] macro_rules! ccase { ($case:ident, $e:expr) => { convert_case::Converter::new() .to_case(convert_case::case!($case)) .convert($e) }; ($from:ident -> $to:ident, $e:expr) => { convert_case::Converter::new() .from_case(convert_case::case!($from)) .to_case(convert_case::case!($to)) .convert($e) }; } #[cfg(test)] mod test { use super::*; use alloc::vec; #[test] fn lossless_against_lossless() { let examples = vec![ (Case::Snake, "my_variable_22_name"), (Case::Constant, "MY_VARIABLE_22_NAME"), (Case::Ada, "My_Variable_22_Name"), (Case::Kebab, "my-variable-22-name"), (Case::Cobol, "MY-VARIABLE-22-NAME"), (Case::Train, "My-Variable-22-Name"), (Case::Pascal, "MyVariable22Name"), (Case::Camel, "myVariable22Name"), (Case::Lower, "my variable 22 name"), (Case::Upper, "MY VARIABLE 22 NAME"), (Case::Title, "My Variable 22 Name"), (Case::Sentence, "My variable 22 name"), ]; for (case_a, str_a) in &examples { for (case_b, str_b) in &examples { assert_eq!(str_b.to_case(*case_a), *str_a); assert_eq!(str_b.from_case(*case_b).to_case(*case_a), *str_a); } } } #[test] fn obvious_default_parsing() { let examples = vec![ "SuperMario64Game", "super-mario64-game", "superMario64 game", "Super Mario 64_game", "SUPERMario 64-game", "super_mario-64 game", ]; for example in examples { assert_eq!(example.to_case(Case::Snake), "super_mario_64_game"); } } #[test] fn multiline_strings() { assert_eq!("one\ntwo\nthree".to_case(Case::Title), "One\ntwo\nthree"); } #[test] fn camel_case_acronyms() { assert_eq!( "XMLHttpRequest".from_case(Case::Camel).to_case(Case::Snake), "xml_http_request" ); assert_eq!( "XMLHttpRequest" .from_case(Case::UpperCamel) .to_case(Case::Snake), "xml_http_request" ); assert_eq!( "XMLHttpRequest" .from_case(Case::Pascal) .to_case(Case::Snake), "xml_http_request" ); } #[test] fn leading_tailing_double_delimiters() { let words = ["first", "second"]; let delimited_cases = &[ Case::Snake, Case::Kebab, Case::Lower, Case::Custom { boundaries: &[Boundary::Custom { condition: |s| *s.get(0).unwrap() == ".", start: 0, len: 1, }], pattern: Pattern::Lowercase, delimiter: ".", }, ]; for &case in delimited_cases { let delim = case.delimiter(); let double = format!("{delim}{delim}"); let identifiers = [ format!("{delim}{}", words.join(delim)), format!("{}{delim}", words.join(delim)), format!("{delim}{}{delim}", words.join(delim)), format!("{}", words.join(&double)), format!("{delim}{}", words.join(&double)), format!("{}{delim}", words.join(&double)), format!("{delim}{}{delim}", words.join(&double)), ]; for identifier in identifiers { assert_eq!(identifier.to_case(case), identifier); assert_eq!(identifier.from_case(case).to_case(case), identifier); } } } #[test] fn early_word_boundaries() { assert_eq!( "aBagel".from_case(Case::Camel).to_case(Case::Snake), "a_bagel" ); } #[test] fn late_word_boundaries() { assert_eq!( "teamA".from_case(Case::Camel).to_case(Case::Snake), "team_a" ); } #[test] fn empty_string() { for (case_a, case_b) in Case::all_cases() .into_iter() .zip(Case::all_cases().into_iter()) { assert_eq!("", "".from_case(*case_a).to_case(*case_b)); } } #[test] fn default_all_boundaries() { assert_eq!( "ABC-abc_abcAbc ABCAbc".to_case(Case::Snake), "abc_abc_abc_abc_abc_abc" ); assert_eq!("8a8A8".to_case(Case::Snake), "8_a_8_a_8"); } #[test] fn remove_boundaries() { assert_eq!( "M02S05BinaryTrees.pdf" .from_case(Case::Pascal) .remove_boundaries(&[Boundary::UpperDigit]) .to_case(Case::Snake), "m02_s05_binary_trees.pdf" ); } #[test] fn with_boundaries() { assert_eq!( "my_dumbFileName" .set_boundaries(&[Boundary::Underscore, Boundary::LowerUpper]) .to_case(Case::Kebab), "my-dumb-file-name" ); } // From issue https://github.com/rutrum/convert-case/issues/4 // From issue https://github.com/rutrum/convert-case/issues/8 #[test] fn unicode_words() { let strings = &["ПЕРСПЕКТИВА24", "música moderna"]; for s in strings { for &case in Case::all_cases() { assert!(!s.to_case(case).is_empty()); } for &from in Case::all_cases() { for &to in Case::all_cases() { assert!(!s.from_case(from).to_case(to).is_empty()); } } } } // idea for asserting the associated boundaries are correct #[test] fn appropriate_associated_boundaries() { let word_groups = &[ vec!["my", "var", "name"], vec!["MY", "var", "Name"], vec!["another", "vAR"], vec!["XML", "HTTP", "Request"], ]; for words in word_groups { for case in Case::all_cases() { if case == &Case::Flat || case == &Case::UpperFlat { continue; } assert_eq!( case.pattern().mutate(&split( &case.pattern().mutate(words).join(case.delimiter()), case.boundaries() )), case.pattern().mutate(words), "Test boundaries on Case::{:?} with {:?}", case, words, ); } } } } convert_case-0.11.0/src/pattern.rs000064400000000000000000000205551046102023000151770ustar 00000000000000use alloc::string::{String, ToString}; use alloc::vec::Vec; fn lowercase_word(word: &str) -> String { word.to_lowercase() } fn uppercase_word(word: &str) -> String { word.to_uppercase() } /// Applies capital pattern to a single word using graphemes. fn capital_word(word: &str) -> String { let mut chars = word.chars(); if let Some(c) = chars.next() { [c.to_uppercase().collect(), chars.as_str().to_lowercase()].concat() } else { String::new() } } /// Transformations on a list of words. /// /// A pattern is a function that maps a list of words into another list /// after changing the casing of each letter. How a patterns mutates /// each letter can be dependent on the word the letters are present in. /// /// ## Custom Pattern /// /// A pattern is a function that maps from a borrowed list of words `&[&str]` to /// an owned list of owned words `Vec` by applying a transformation. /// One example of custom behavior might be a pattern /// that detects a fixed list of two-letter acronyms, and capitalizes them /// appropriately on output. /// ``` /// use convert_case::{Converter, Pattern}; /// /// fn pascal_upper_acronyms(words: &[&str]) -> Vec { /// Pattern::Capital.mutate(words).into_iter() /// .map(|word| match word.as_ref() { /// "Io" | "Xml" => word.to_uppercase(), /// _ => word, /// }) /// .collect() /// } /// /// let acronym_converter = Converter::new() /// .set_patterns(&[Pattern::Custom(pascal_upper_acronyms)]); /// /// assert_eq!(acronym_converter.convert("io_stream"), "IOStream"); /// assert_eq!(acronym_converter.convert("xml request"), "XMLRequest"); /// ``` /// /// Another example might be a one that explicitly adds leading /// and trailing double underscores. We do this by modifying the words directly, /// which will get passed as-is to the join function. /// ``` /// use convert_case::{Converter, Pattern}; /// /// fn snake_dunder(mut words: &[&str]) -> Vec { /// words /// .into_iter() /// .map(|word| word.to_lowercase()) /// .enumerate() /// .map(|(i, word)| { /// if words.len() == 1 { /// format!("__{}__", word) /// } else if i == 0 { /// format!("__{}", word) /// } else if i == words.len() - 1 { /// format!("{}__", word) /// } else { /// word /// } /// }) /// .collect() /// } /// /// let dunder_converter = Converter::new() /// .set_patterns(&[Pattern::Custom(snake_dunder)]) /// .set_delimiter("_"); /// /// assert_eq!(dunder_converter.convert("getAttr"), "__get_attr__"); /// assert_eq!(dunder_converter.convert("ITER"), "__iter__"); /// ``` #[derive(Debug, Copy, Clone)] pub enum Pattern { /// Makes all words lowercase. /// ``` /// # use convert_case::Pattern; /// assert_eq!( /// Pattern::Lowercase.mutate(&["Case", "CONVERSION", "library"]), /// vec!["case", "conversion", "library"], /// ); /// ``` Lowercase, /// Makes all words uppercase. /// ``` /// # use convert_case::Pattern; /// assert_eq!( /// Pattern::Uppercase.mutate(&["Case", "CONVERSION", "library"]), /// vec!["CASE", "CONVERSION", "LIBRARY"], /// ); /// ``` Uppercase, /// Makes the first letter of each word uppercase /// and the remaining letters of each word lowercase. /// ``` /// # use convert_case::Pattern; /// assert_eq!( /// Pattern::Capital.mutate(&["Case", "CONVERSION", "library"]), /// vec!["Case", "Conversion", "Library"], /// ); /// ``` Capital, /// Makes the first non-empty word lowercase and the /// remaining capitalized. /// ``` /// # use convert_case::Pattern; /// assert_eq!( /// Pattern::Camel.mutate(&["Case", "CONVERSION", "library"]), /// vec!["case", "Conversion", "Library"], /// ); /// ``` Camel, /// Makes the first non-empty word capitalized and the /// remaining lowercase. /// ``` /// # use convert_case::Pattern; /// assert_eq!( /// Pattern::Sentence.mutate(&["Case", "CONVERSION", "library"]), /// vec!["Case", "conversion", "library"], /// ); /// ``` Sentence, /// Filters out empty words from the list. /// Useful when splitting produces empty words from leading/trailing/duplicate delimiters. /// ``` /// # use convert_case::Pattern; /// assert_eq!( /// Pattern::RemoveEmpty.mutate(&["", "first", "", "second", ""]), /// vec!["first", "second"], /// ); /// ``` RemoveEmpty, /// Define custom behavior to transform a set of words. /// /// See the [`Pattern`] documentation for examples. Custom(fn(&[&str]) -> Vec), } impl Pattern { /// Applies the pattern transformation to a list of words. pub fn mutate>(&self, words: &[S]) -> Vec { use Pattern::*; match self { Custom(transformation) => { let borrowed: Vec<&str> = words.iter().map(|s| s.as_ref()).collect(); (transformation)(&borrowed) } Lowercase => words .iter() .map(|word| lowercase_word(word.as_ref())) .collect(), Uppercase => words .iter() .map(|word| uppercase_word(word.as_ref())) .collect(), Capital => words .iter() .map(|word| capital_word(word.as_ref())) .collect(), Camel => words .iter() .enumerate() .map(|(i, word)| { if i == 0 { lowercase_word(word.as_ref()) } else { capital_word(word.as_ref()) } }) .collect(), Sentence => words .iter() .enumerate() .map(|(i, word)| { if i == 0 { capital_word(word.as_ref()) } else { lowercase_word(word.as_ref()) } }) .collect(), RemoveEmpty => words .iter() .filter(|word| !word.as_ref().is_empty()) .map(|word| word.as_ref().to_string()) .collect(), } } } impl PartialEq for Pattern { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Lowercase, Self::Lowercase) => true, (Self::Uppercase, Self::Uppercase) => true, (Self::Capital, Self::Capital) => true, (Self::Camel, Self::Camel) => true, (Self::Sentence, Self::Sentence) => true, (Self::RemoveEmpty, Self::RemoveEmpty) => true, // Custom patterns are never equal because they contain function pointers, // which cannot be reliably compared. (Self::Custom(_), Self::Custom(_)) => false, _ => false, } } } impl Eq for Pattern {} impl core::hash::Hash for Pattern { fn hash(&self, state: &mut H) { // Hash the discriminant for all variants core::mem::discriminant(self).hash(state); // Custom variants only hash the discriminant since they can't be meaningfully compared } } #[cfg(test)] mod test { use crate::Case; use crate::Converter; use super::*; #[test] fn mutate_empty_strings() { for word_pattern in [lowercase_word, uppercase_word, capital_word] { assert_eq!(String::new(), word_pattern(&String::new())) } } #[test] fn filtering_with_remove_empty() { let conv = Converter::new() .from_case(Case::Kebab) .set_patterns(&[Pattern::RemoveEmpty, Pattern::Camel]); assert_eq!(conv.convert("--leading-delims"), "leadingDelims"); } #[test] fn remove_empty_pattern() { assert_eq!( Pattern::RemoveEmpty.mutate(&["", "first", "", "second", ""]), vec!["first", "second"] ); assert_eq!(Pattern::RemoveEmpty.mutate(&["only"]), vec!["only"]); assert_eq!( Pattern::RemoveEmpty.mutate(&["", "", ""]), Vec::::new() ); } } convert_case-0.11.0/tests/macros.rs000064400000000000000000000016061046102023000153550ustar 00000000000000use convert_case::ccase; use convert_case::{separator, split}; #[test] fn ccase_to_case() { assert_eq!(ccase!(snake, "my_Var_Name"), "my_var_name"); assert_eq!(ccase!(constant, "my_Var_Name"), "MY_VAR_NAME"); assert_eq!(ccase!(kebab, "my_Var_Name"), "my-var-name"); assert_eq!(ccase!(kebab, String::from("my_Var_Name")), "my-var-name"); } #[test] fn ccase_from_to_case() { assert_eq!(ccase!(kebab -> camel, "myVar-name_var"), "myvarName_var"); assert_eq!(ccase!(snake -> pascal, "my-var_name-var"), "My-varName-var"); } #[test] fn separator_custom_delimiters() { let dot = separator!("."); assert_eq!( split(&"lower.Upper.Upper", &[dot]), vec!["lower", "Upper", "Upper"] ); let double_colon = separator!("::"); assert_eq!( split(&"lower::lowerUpper::Upper", &[double_colon]), vec!["lower", "lowerUpper", "Upper"] ); } convert_case-0.11.0/tests/string_types.rs000064400000000000000000000022401046102023000166160ustar 00000000000000use convert_case::{Case, Casing}; use std::rc::Rc; use std::sync::Arc; macro_rules! test_casing_on_type { ($name:ident, $constructor:expr) => { #[test] fn $name() { let s = $constructor("rust_programming-language"); assert_eq!(s.to_case(Case::Pascal), "RustProgrammingLanguage"); assert_eq!( s.from_case(Case::Kebab).to_case(Case::Pascal), "Rust_programmingLanguage" ); } }; } test_casing_on_type!(string_type, String::from); test_casing_on_type!(rc_str_type, Rc::::from); test_casing_on_type!(arc_str_type, Arc::::from); #[test] fn str_type() { let s: &str = "rust_programming-language"; assert_eq!(s.to_case(Case::Pascal), "RustProgrammingLanguage"); assert_eq!( s.from_case(Case::Kebab).to_case(Case::Pascal), "Rust_programmingLanguage" ); } #[test] fn string_ref_type() { let s: String = String::from("rust_programming-language"); assert_eq!((&s).to_case(Case::Pascal), "RustProgrammingLanguage"); assert_eq!( s.from_case(Case::Kebab).to_case(Case::Pascal), "Rust_programmingLanguage" ); }