├── .github └── workflows │ └── TestBox.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── box.json ├── server-sqids-coldfusion-adobe2018.json ├── server-sqids-coldfusion-adobe2021.json ├── server-sqids-coldfusion-adobe2023.json ├── server-sqids-coldfusion-lucee5.json ├── server-sqids-coldfusion-lucee6.json ├── src └── Sqids │ ├── SqidsEncoder.cfc │ ├── SqidsOptions.cfc │ └── blocklist.json ├── testing.md └── tests ├── Application.cfc ├── runner.cfm └── specs ├── AlphabetTest.cfc ├── BlocklistTest.cfc ├── EncodingTest.cfc └── MinLengthTest.cfc /.github/workflows/TestBox.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the master branch 5 | on: 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | jobs: 12 | test: 13 | name: Test on ${{ matrix.cfml_engine }} 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | cfml_engine: [lucee@5, lucee@6, adobe@2018, adobe@2021, adobe@2023 ] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | - name: Run TestBox 24 | uses: coldfumonkeh/cfml-testbox-action@v1 25 | with: 26 | cfml-engine: ${{ matrix.cfml_engine }} 27 | - name: Publish Test Report 28 | uses: mikepenz/action-junit-report@v4 29 | if: always() 30 | with: 31 | annotate_only: true 32 | detailed_summary: true 33 | check_name: 'TestBox report' 34 | report_paths: 'testbox.xml' 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | .idea 3 | *.iml 4 | testbox 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | 9 | ## [0.0.1] - 2024-02-02 10 | 11 | ### Added 12 | 13 | - Initial implementation of [the spec](https://github.com/sqids/sqids-spec) 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present Sqids maintainers. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Sqids ColdFusion](https://sqids.org/coldfusion) 2 | 3 | [![Tests](https://github.com/sqids/sqids-coldfusion/actions/workflows/TestBox.yml/badge.svg)](https://github.com/sqids/sqids-coldfusion/actions/workflows/TestBox.yml) 4 | 5 | [Sqids](https://sqids.org/coldfusion) (*pronounced "squids"*) is a small library that lets you **generate unique IDs from numbers**. It's good for link shortening, fast & URL-safe ID generation and decoding back into numbers for quicker database lookups. 6 | 7 | Features: 8 | 9 | - **Encode multiple numbers** - generate short IDs from one or several non-negative numbers 10 | - **Quick decoding** - easily decode IDs back into numbers 11 | - **Unique IDs** - generate unique IDs by shuffling the alphabet once 12 | - **ID padding** - provide minimum length to make IDs more uniform 13 | - **URL safe** - auto-generated IDs do not contain common profanity 14 | - **Randomized output** - Sequential input provides nonconsecutive IDs 15 | - **Many implementations** - Support for [40+ programming languages](https://sqids.org/) 16 | 17 | ## 🧰 Use-cases 18 | 19 | Good for: 20 | 21 | - Generating IDs for public URLs (eg: link shortening) 22 | - Generating IDs for internal systems (eg: event tracking) 23 | - Decoding for quicker database lookups (eg: by primary keys) 24 | 25 | Not good for: 26 | 27 | - Sensitive data (this is not an encryption library) 28 | - User IDs (can be decoded revealing user count) 29 | 30 | ## 🚀 Getting started 31 | 32 | Tested with: 33 | * Adobe ColdFusion 2018 34 | * Adobe ColdFusion 2021 35 | * Adobe ColdFusion 2023 36 | * Lucee 5 37 | * Lucee 6 38 | 39 | Clone the repository and copy the Sqids folder to your project. Then in the application.cfc add a mapping 40 | ```java 41 | this.mappings[ "/Sqids" ] = expandPath( "/src/Sqids" ); 42 | ``` 43 | 44 | ## 👩‍💻 Examples 45 | 46 | Simple encode & decode: 47 | 48 | ```java 49 | var sqids = new Sqids.SquidsEncoder(); 50 | var id = sqids.encode([1, 2, 3]); // "86Rf07" 51 | var numbers = sqids.decode(id); // [1, 2, 3] 52 | ``` 53 | 54 | > **Note** 55 | > 🚧 Because of the algorithm's design, **multiple IDs can decode back into the same sequence of numbers**. If it's important to your design that IDs are canonical, you have to manually re-encode decoded numbers and check that the generated ID matches. 56 | 57 | Enforce a *minimum* length for IDs: 58 | 59 | ```java 60 | var sqids = new Sqids.SquidsEncoder(new Sqids.SqidsOptions(minLength = 10)); 61 | var id = sqids.encode([1, 2, 3]); // "86Rf07xd4z" 62 | var numbers = sqids.decode(id); // [1, 2, 3] 63 | ``` 64 | 65 | Randomize IDs by providing a custom alphabet: 66 | 67 | ```java 68 | var sqids = new Sqids.SquidsEncoder(new Sqids.SqidsOptions(alphabet = "FxnXM1kBN6cuhsAvjW3Co7l2RePyY8DwaU04Tzt9fHQrqSVKdpimLGIJOgb5ZE")); 69 | var id = sqids.encode([1, 2, 3]); // "B4aajs" 70 | var numbers = sqids.decode(id); // [1, 2, 3] 71 | ``` 72 | 73 | Prevent specific words from appearing anywhere in the auto-generated IDs: 74 | 75 | ```java 76 | var sqids = new Sqids.SquidsEncoder(new Sqids.SqidsOptions(blocklist = ["86Rf07"])); 77 | var id = sqids.encode([1, 2, 3]); // "se8ojk" 78 | var numbers = sqids.decode(id); // [1, 2, 3] 79 | ``` 80 | 81 | ## 📝 License 82 | 83 | [MIT](LICENSE) 84 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies":{ 3 | "testbox":"^5.3.1+6" 4 | }, 5 | "installPaths":{ 6 | "testbox":"testbox/" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /server-sqids-coldfusion-adobe2018.json: -------------------------------------------------------------------------------- 1 | { 2 | "app":{ 3 | "cfengine":"adobe@2018" 4 | }, 5 | "name":"sqids-coldfusion-adobe2018", 6 | "openBrowser":"false", 7 | "web":{ 8 | "HTTP":{ 9 | "port":"60852" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server-sqids-coldfusion-adobe2021.json: -------------------------------------------------------------------------------- 1 | { 2 | "app":{ 3 | "cfengine":"adobe@2021" 4 | }, 5 | "name":"sqids-coldfusion-adobe2021", 6 | "openBrowser":"false", 7 | "web":{ 8 | "HTTP":{ 9 | "port":"60853" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server-sqids-coldfusion-adobe2023.json: -------------------------------------------------------------------------------- 1 | { 2 | "app":{ 3 | "cfengine":"adobe@2023" 4 | }, 5 | "name":"sqids-coldfusion-adobe20123", 6 | "openBrowser":"false", 7 | "web":{ 8 | "HTTP":{ 9 | "port":"60854" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server-sqids-coldfusion-lucee5.json: -------------------------------------------------------------------------------- 1 | { 2 | "app":{ 3 | "cfengine":"lucee@5" 4 | }, 5 | "name":"sqids-coldfusion-lucee5", 6 | "openBrowser":"false", 7 | "web":{ 8 | "HTTP":{ 9 | "port":"60850" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server-sqids-coldfusion-lucee6.json: -------------------------------------------------------------------------------- 1 | { 2 | "app":{ 3 | "cfengine":"lucee@6" 4 | }, 5 | "name":"sqids-coldfusion-lucee6", 6 | "openBrowser":"false", 7 | "web":{ 8 | "HTTP":{ 9 | "port":"60851" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Sqids/SqidsEncoder.cfc: -------------------------------------------------------------------------------- 1 | component namespace="Sqids" 2 | { 3 | public SqidsEncoder function init(SqidsOptions options) { 4 | variables.MinAlphabetLength = 3; 5 | variables.MaxNumber = createObject("java", "java.lang.Integer").MAX_VALUE; 6 | 7 | if (isNull(arguments.options)) { 8 | arguments.options = new SqidsOptions(); 9 | } 10 | 11 | // Initialize properties based on provided options or use defaults 12 | var alphabet = arguments.options.getAlphabet(); 13 | var minLength = arguments.options.getMinLength(); 14 | var blocklist = arguments.options.getBlocklist(); 15 | 16 | // alphabet cannot contain multibyte characters 17 | if (len(charsetDecode(alphabet, "UTF-8")) != len(alphabet)) { 18 | throw(type = "custom", message = "Alphabet cannot contain multibyte characters"); 19 | } 20 | 21 | // check the length of the alphabet 22 | if (alphabet.len() < MinAlphabetLength) { 23 | throw(type = "custom", message = "Alphabet length must be at least 3"); 24 | } 25 | 26 | // check that the alphabet has only unique characters 27 | if (len(removeDuplicates(alphabet)) != alphabet.len()) { 28 | throw(type = "custom", message = "Alphabet must contain unique characters"); 29 | } 30 | 31 | // clean up blocklist: 32 | // 1. all blocklist words should be lowercase 33 | // 2. no words less than 3 chars 34 | // 3. if some words contain chars that are not in the alphabet, remove those 35 | var filteredBlocklist = []; 36 | var alphabetChars = listToArray(lCase(alphabet), ""); 37 | for (var word in blocklist) { 38 | if (word.len() >= 3) { 39 | var wordLowercased = lCase(word); 40 | var wordChars = listToArray(wordLowercased, ""); 41 | var intersection = wordChars.filter( 42 | function (required string wordChar) 43 | { 44 | var _wordChar = arguments.wordChar; 45 | return alphabetChars.some( 46 | function (required string alphabetChar) 47 | { 48 | return arguments.alphabetChar == _wordChar; 49 | }); 50 | }); 51 | if (intersection.len() == wordChars.len()) { 52 | filteredBlocklist.append(wordLowercased); 53 | } 54 | } 55 | } 56 | 57 | variables.alphabet = shuffle(listToArray(alphabet, "")); 58 | variables.minLength = minLength; 59 | variables.blocklist = filteredBlocklist; 60 | 61 | return this; 62 | } 63 | 64 | /** 65 | * Encodes an array of unsigned integers into an ID 66 | * 67 | * These are the cases where encoding might fail: 68 | * - One of the numbers passed is smaller than 0 or greater than `maxValue()` 69 | * - An n-number of attempts has been made to re-generated the ID, where n is alphabet length + 1 70 | * 71 | * @param {array.} numbers Non-negative integers to encode into an ID 72 | * @returns {string} Generated ID 73 | */ 74 | public string function encode(required array numbersArg) { 75 | var numbers = arguments.numbersArg; 76 | 77 | // if no numbers passed, return an empty string 78 | if (arrayLen(numbers) == 0) { 79 | return ""; 80 | } 81 | 82 | // don"t allow out-of-range numbers [might be lang-specific] 83 | var inRangeNumbers = numbers.filter( 84 | function (required numeric n) 85 | { 86 | return arguments.n >= 0 && arguments.n <= variables.MaxNumber; 87 | }); 88 | 89 | if (inRangeNumbers.len() != numbers.len()) { 90 | throw(type="custom", message="Encoding supports numbers between 0 and #variables.MaxNumber#"); 91 | } 92 | 93 | return encodeNumbers(numbers); 94 | } 95 | 96 | /** 97 | * Internal function that encodes an array of unsigned integers into an ID 98 | * 99 | * @param {array.} numbers Non-negative integers to encode into an ID 100 | * @param {number} increment An internal number used to modify the `offset` variable in order to re-generate the ID 101 | * @returns {string} Generated ID 102 | */ 103 | private string function encodeNumbers(required array numbersArg, numeric increment = 0) { 104 | var numbers = arguments.numbersArg; 105 | 106 | // if increment is greater than alphabet length, we"ve reached max attempts 107 | if (increment > variables.alphabet.len()) { 108 | throw(type="custom", message="Reached max attempts to re-generate the ID"); 109 | } 110 | 111 | // get a semi-random offset from input numbers 112 | var offset = numbers.reduce( 113 | function (required numeric result, required numeric item, required numeric index) 114 | { 115 | return asc(variables.alphabet[arguments.item mod variables.alphabet.len() + 1]) + arguments.index - 1 + arguments.result; 116 | }, numbers.len() 117 | ) mod variables.alphabet.len() + 1; 118 | 119 | // if there is a non-zero `increment`, it's an internal attempt to re-generated the ID 120 | offset = (offset + arguments.increment) mod variables.alphabet.len(); 121 | 122 | // re-arrange alphabet so that second-half goes in front of the first-half 123 | var alphabet = variables.alphabet; 124 | if (offset > 1) { 125 | alphabet = variables.alphabet.slice(offset); 126 | alphabet.append(variables.alphabet.slice(1, offset - 1), true); 127 | } 128 | 129 | // `prefix` is the first character in the generated ID, used for randomization 130 | var prefix = alphabet[1]; 131 | 132 | // reverse alphabet (otherwise for [0, x] `offset` and `separator` will be the same char) 133 | alphabet = listToArray(reverse(arrayToList(alphabet, "")), ""); 134 | 135 | // final ID will always have the `prefix` character at the beginning 136 | var ret = [prefix]; 137 | 138 | // encode input array 139 | for (var i = 1; i <= numbers.len(); i++) { 140 | var num = numbers[i]; 141 | 142 | // the first character of the alphabet is going to be reserved for the `separator` 143 | var alphabetWithoutSeparator = alphabet.slice(2); 144 | ret.append(toId(num, alphabetWithoutSeparator), true); 145 | 146 | // if not the last number 147 | if (i < numbers.len()) { 148 | // `separator` character is used to isolate numbers within the ID 149 | ret.append(alphabet[1]); 150 | 151 | // shuffle on every iteration 152 | alphabet = shuffle(alphabet); 153 | } 154 | } 155 | 156 | // handle `minLength` requirement, if the ID is too short 157 | if (variables.minLength > ret.len()) { 158 | // append a separator 159 | ret.append(alphabet[1]); 160 | 161 | // keep appending `separator` + however much alphabet is needed 162 | // for decoding: two separators next to each other is what tells us the rest are junk characters 163 | while (variables.minLength - ret.len() > 0) { 164 | alphabet = shuffle(alphabet); 165 | ret.append(alphabet.slice(1, min(variables.minLength - ret.len(), alphabet.len())), true); 166 | } 167 | } 168 | 169 | var id = arrayToList(ret, ""); 170 | 171 | // if ID has a blocked word anywhere, restart with a +1 increment 172 | if (isBlockedId(id)) { 173 | id = encodeNumbers(numbers, increment + 1); 174 | } 175 | 176 | return id; 177 | } 178 | 179 | /** 180 | * Decodes an ID back into an array of unsigned integers 181 | * 182 | * These are the cases where the return value might be an empty array: 183 | * - Empty ID / empty string 184 | * - Non-alphabet character is found within ID 185 | * 186 | * @param {string} id Encoded ID 187 | * @returns {array.} Array of unsigned integers 188 | */ 189 | public array function decode(required string idArg) { 190 | var id = arguments.idArg; 191 | var ret = []; 192 | 193 | // if an empty string, return an empty array 194 | if (id == "") { 195 | return ret; 196 | } 197 | 198 | // if a character is not in the alphabet, return an empty array 199 | var alphabetChars = variables.alphabet; 200 | for (var idChar in listToArray(id, "")) { 201 | if (!alphabetChars.some( 202 | function (required string alphabetChar) 203 | { 204 | return arguments.alphabetChar == idChar; 205 | })) { 206 | return ret; 207 | } 208 | } 209 | 210 | // first character is always the `prefix` 211 | var prefix = id[1]; 212 | 213 | // `offset` is the semi-random position that was generated during encoding 214 | var offset = variables.alphabet.find(prefix); 215 | 216 | // re-arrange alphabet back into it's original form 217 | var alphabet = variables.alphabet; 218 | if (offset > 1) { 219 | alphabet = variables.alphabet.slice(offset); 220 | alphabet.append(variables.alphabet.slice(1, offset - 1), true); 221 | } 222 | 223 | // reverse alphabet 224 | alphabet = listToArray(reverse(arrayToList(alphabet, "")), ""); 225 | 226 | // now it's safe to remove the prefix character from ID, it's not needed anymore 227 | id = id.right(id.len() - 1); 228 | 229 | while (id.len()) { 230 | var separator = alphabet[1]; 231 | // we need the first part to the left of the separator to decode the number 232 | var chunks = listToArray(id, separator, true); 233 | 234 | // if chunk is empty, we are done (the rest are junk characters) 235 | if (chunks[1] == "") { 236 | return ret; 237 | } 238 | 239 | // decode the number without using the `separator` character 240 | var alphabetWithoutSeparator = alphabet.slice(2); 241 | ret.append(toNumber(chunks[1], alphabetWithoutSeparator), true); 242 | 243 | // if this ID has multiple numbers, shuffle the alphabet because that's what encoding function did 244 | if (chunks.len() > 1) { 245 | alphabet = shuffle(alphabet); 246 | } 247 | 248 | // `id` is now going to be everything to the right of the `separator` 249 | if (chunks.len() > 1) { 250 | id = arrayToList(chunks.slice(2), separator); 251 | } else { 252 | id = ""; 253 | } 254 | } 255 | 256 | return ret; 257 | } 258 | 259 | // consistent shuffle (always produces the same result given the input) 260 | private array function shuffle(array alphabet) { 261 | var chars = arguments.alphabet; 262 | var numberOfChars = chars.len(); 263 | 264 | var i = 1; 265 | for (var j = numberOfChars; j > 1; j--) { 266 | var r = (i * j + asc(chars[i]) + asc(chars[j])) mod numberOfChars + 1; 267 | var temp = chars[i]; 268 | chars[i] = chars[r]; 269 | chars[r] = temp; 270 | 271 | i++; 272 | } 273 | 274 | return chars; 275 | } 276 | 277 | private array function toId(required numeric num, required array alphabet) { 278 | var id = []; 279 | var chars = arguments.alphabet; 280 | var result = arguments.num; 281 | 282 | do { 283 | id.prepend(chars[result mod chars.len() + 1]); 284 | result = Int(result / chars.len()); 285 | } while (result > 0); 286 | 287 | return id; 288 | } 289 | 290 | private numeric function toNumber(required string id, required array alphabetArg) 291 | { 292 | var alphabet = arguments.alphabetArg; 293 | var idChars = listToArray(arguments.id, ""); 294 | 295 | return idChars.reduce( 296 | function (required numeric result, required string item) 297 | { 298 | return arguments.result * alphabet.len() + alphabet.find(arguments.item) - 1; 299 | }, 0); 300 | } 301 | 302 | private boolean function isBlockedId(required string idArg) { 303 | var id = lCase(arguments.idArg); 304 | 305 | for (var word in variables.blocklist) { 306 | // no point in checking words that are longer than the ID 307 | if (word.len() <= id.len()) { 308 | if (id.len() <= 3 || word.len() <= 3) { 309 | // short words have to match completely; otherwise, too many matches 310 | if (id == word) { 311 | return true; 312 | } 313 | } else if (ReFind("\d", word) > 0) { // test for a number in the word 314 | // words with leet speak replacements are visible mostly on the ends of the ID 315 | if (Left(id, word.len()) == word || Right(id, word.len()) == word) { 316 | return true; 317 | } 318 | } else if (findNoCase(word, id) > 0) { 319 | // otherwise, check for blocked word anywhere in the string 320 | return true; 321 | } 322 | } 323 | } 324 | 325 | return false; 326 | } 327 | 328 | /* Helper functions */ 329 | private string function removeDuplicates(required string value) { 330 | if (isLucee()) { 331 | return ListRemoveDuplicates(value, ""); 332 | } 333 | 334 | var returnValue = ""; 335 | for (var i = 1; i <= value.len(); i++) { 336 | if (find(value[i], returnValue) == 0) { 337 | returnValue &= value[i]; 338 | } 339 | } 340 | 341 | return returnValue; 342 | } 343 | 344 | private function isLucee(){ 345 | return ( structKeyExists( server, "lucee" ) ); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/Sqids/SqidsOptions.cfc: -------------------------------------------------------------------------------- 1 | component namespace="Sqids" accessors=true 2 | { 3 | property name ="alphabet" type=string setter=false; 4 | property name ="minLength" type=numeric setter=false; 5 | property name ="blocklist" type=array setter=false; 6 | 7 | public SqidsOptions function init(string alphabet, string minLength, array blocklist) 8 | { 9 | variables.alphabet = isNull(arguments.alphabet) 10 | ? "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 11 | : arguments.alphabet; 12 | 13 | variables.minLength = isNull(arguments.minLength) 14 | ? 0 15 | : arguments.minLength; 16 | 17 | variables.blocklist = isNull(arguments.blocklist) 18 | ? deserializeJSON(fileRead(getCanonicalPath(getDirectoryFromPath(getCurrentTemplatePath()) & "/blocklist.json"))) 19 | : arguments.blocklist; 20 | 21 | return this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Sqids/blocklist.json: -------------------------------------------------------------------------------- 1 | ["0rgasm","1d10t","1d1ot","1di0t","1diot","1eccacu10","1eccacu1o","1eccacul0","1eccaculo","1mbec11e","1mbec1le","1mbeci1e","1mbecile","a11upat0","a11upato","a1lupat0","a1lupato","aand","ah01e","ah0le","aho1e","ahole","al1upat0","al1upato","allupat0","allupato","ana1","ana1e","anal","anale","anus","arrapat0","arrapato","arsch","arse","ass","b00b","b00be","b01ata","b0ceta","b0iata","b0ob","b0obe","b0sta","b1tch","b1te","b1tte","ba1atkar","balatkar","bastard0","bastardo","batt0na","battona","bitch","bite","bitte","bo0b","bo0be","bo1ata","boceta","boiata","boob","boobe","bosta","bran1age","bran1er","bran1ette","bran1eur","bran1euse","branlage","branler","branlette","branleur","branleuse","c0ck","c0g110ne","c0g11one","c0g1i0ne","c0g1ione","c0gl10ne","c0gl1one","c0gli0ne","c0glione","c0na","c0nnard","c0nnasse","c0nne","c0u111es","c0u11les","c0u1l1es","c0u1lles","c0ui11es","c0ui1les","c0uil1es","c0uilles","c11t","c11t0","c11to","c1it","c1it0","c1ito","cabr0n","cabra0","cabrao","cabron","caca","cacca","cacete","cagante","cagar","cagare","cagna","cara1h0","cara1ho","caracu10","caracu1o","caracul0","caraculo","caralh0","caralho","cazz0","cazz1mma","cazzata","cazzimma","cazzo","ch00t1a","ch00t1ya","ch00tia","ch00tiya","ch0d","ch0ot1a","ch0ot1ya","ch0otia","ch0otiya","ch1asse","ch1avata","ch1er","ch1ng0","ch1ngadaz0s","ch1ngadazos","ch1ngader1ta","ch1ngaderita","ch1ngar","ch1ngo","ch1ngues","ch1nk","chatte","chiasse","chiavata","chier","ching0","chingadaz0s","chingadazos","chingader1ta","chingaderita","chingar","chingo","chingues","chink","cho0t1a","cho0t1ya","cho0tia","cho0tiya","chod","choot1a","choot1ya","chootia","chootiya","cl1t","cl1t0","cl1to","clit","clit0","clito","cock","cog110ne","cog11one","cog1i0ne","cog1ione","cogl10ne","cogl1one","cogli0ne","coglione","cona","connard","connasse","conne","cou111es","cou11les","cou1l1es","cou1lles","coui11es","coui1les","couil1es","couilles","cracker","crap","cu10","cu1att0ne","cu1attone","cu1er0","cu1ero","cu1o","cul0","culatt0ne","culattone","culer0","culero","culo","cum","cunt","d11d0","d11do","d1ck","d1ld0","d1ldo","damn","de1ch","deich","depp","di1d0","di1do","dick","dild0","dildo","dyke","encu1e","encule","enema","enf01re","enf0ire","enfo1re","enfoire","estup1d0","estup1do","estupid0","estupido","etr0n","etron","f0da","f0der","f0ttere","f0tters1","f0ttersi","f0tze","f0utre","f1ca","f1cker","f1ga","fag","fica","ficker","figa","foda","foder","fottere","fotters1","fottersi","fotze","foutre","fr0c10","fr0c1o","fr0ci0","fr0cio","fr0sc10","fr0sc1o","fr0sci0","fr0scio","froc10","froc1o","froci0","frocio","frosc10","frosc1o","frosci0","froscio","fuck","g00","g0o","g0u1ne","g0uine","gandu","go0","goo","gou1ne","gouine","gr0gnasse","grognasse","haram1","harami","haramzade","hund1n","hundin","id10t","id1ot","idi0t","idiot","imbec11e","imbec1le","imbeci1e","imbecile","j1zz","jerk","jizz","k1ke","kam1ne","kamine","kike","leccacu10","leccacu1o","leccacul0","leccaculo","m1erda","m1gn0tta","m1gnotta","m1nch1a","m1nchia","m1st","mam0n","mamahuev0","mamahuevo","mamon","masturbat10n","masturbat1on","masturbate","masturbati0n","masturbation","merd0s0","merd0so","merda","merde","merdos0","merdoso","mierda","mign0tta","mignotta","minch1a","minchia","mist","musch1","muschi","n1gger","neger","negr0","negre","negro","nerch1a","nerchia","nigger","orgasm","p00p","p011a","p01la","p0l1a","p0lla","p0mp1n0","p0mp1no","p0mpin0","p0mpino","p0op","p0rca","p0rn","p0rra","p0uff1asse","p0uffiasse","p1p1","p1pi","p1r1a","p1rla","p1sc10","p1sc1o","p1sci0","p1scio","p1sser","pa11e","pa1le","pal1e","palle","pane1e1r0","pane1e1ro","pane1eir0","pane1eiro","panele1r0","panele1ro","paneleir0","paneleiro","patakha","pec0r1na","pec0rina","pecor1na","pecorina","pen1s","pendej0","pendejo","penis","pip1","pipi","pir1a","pirla","pisc10","pisc1o","pisci0","piscio","pisser","po0p","po11a","po1la","pol1a","polla","pomp1n0","pomp1no","pompin0","pompino","poop","porca","porn","porra","pouff1asse","pouffiasse","pr1ck","prick","pussy","put1za","puta","puta1n","putain","pute","putiza","puttana","queca","r0mp1ba11e","r0mp1ba1le","r0mp1bal1e","r0mp1balle","r0mpiba11e","r0mpiba1le","r0mpibal1e","r0mpiballe","rand1","randi","rape","recch10ne","recch1one","recchi0ne","recchione","retard","romp1ba11e","romp1ba1le","romp1bal1e","romp1balle","rompiba11e","rompiba1le","rompibal1e","rompiballe","ruff1an0","ruff1ano","ruffian0","ruffiano","s1ut","sa10pe","sa1aud","sa1ope","sacanagem","sal0pe","salaud","salope","saugnapf","sb0rr0ne","sb0rra","sb0rrone","sbattere","sbatters1","sbattersi","sborr0ne","sborra","sborrone","sc0pare","sc0pata","sch1ampe","sche1se","sche1sse","scheise","scheisse","schlampe","schwachs1nn1g","schwachs1nnig","schwachsinn1g","schwachsinnig","schwanz","scopare","scopata","sexy","sh1t","shit","slut","sp0mp1nare","sp0mpinare","spomp1nare","spompinare","str0nz0","str0nza","str0nzo","stronz0","stronza","stronzo","stup1d","stupid","succh1am1","succh1ami","succhiam1","succhiami","sucker","t0pa","tapette","test1c1e","test1cle","testic1e","testicle","tette","topa","tr01a","tr0ia","tr0mbare","tr1ng1er","tr1ngler","tring1er","tringler","tro1a","troia","trombare","turd","twat","vaffancu10","vaffancu1o","vaffancul0","vaffanculo","vag1na","vagina","verdammt","verga","w1chsen","wank","wichsen","x0ch0ta","x0chota","xana","xoch0ta","xochota","z0cc01a","z0cc0la","z0cco1a","z0ccola","z1z1","z1zi","ziz1","zizi","zocc01a","zocc0la","zocco1a","zoccola"] 2 | -------------------------------------------------------------------------------- /testing.md: -------------------------------------------------------------------------------- 1 | To run the unit tests install [commandbox](https://www.ortussolutions.com/products/commandbox). 2 | 3 | Then open the `box` command and `cd` to this directory. 4 | 5 | Then run the following: 6 | ```shell 7 | package install 8 | server start serverconfigfile=server-sqids-coldfusion-lucee5.json 9 | testbox run http://localhost:60850/tests/runner 10 | ``` 11 | -------------------------------------------------------------------------------- /tests/Application.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright Since 2005 Ortus Solutions, Corp 3 | * www.ortussolutions.com 4 | * ************************************************************************************* 5 | */ 6 | component { 7 | 8 | this.name = "A TestBox Runner Suite " & hash( getCurrentTemplatePath() ); 9 | // any other application.cfc stuff goes below: 10 | this.sessionManagement = true; 11 | 12 | // any mappings go here, we create one that points to the root called test. 13 | this.mappings[ "/tests" ] = getDirectoryFromPath( getCurrentTemplatePath() ); 14 | this.mappings[ "/Sqids" ] = expandPath( "/src/Sqids" ); 15 | 16 | // any orm definitions go here. 17 | 18 | // request start 19 | public boolean function onRequestStart( String targetPage ){ 20 | return true; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /tests/runner.cfm: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /tests/specs/AlphabetTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | /*********************************** LIFE CYCLE Methods ***********************************/ 4 | 5 | function beforeAll() { 6 | // setup the entire test bundle here 7 | } 8 | 9 | function afterAll() { 10 | // do cleanup here 11 | } 12 | 13 | /*********************************** BDD SUITES ***********************************/ 14 | 15 | function run() { 16 | /** 17 | * describe() starts a suite group of spec tests. It is the main BDD construct. 18 | * You can also use the aliases: story(), feature(), scenario(), given(), when() 19 | * to create fluent chains of human-readable expressions. 20 | * 21 | * Arguments: 22 | * 23 | * @title Required: The title of the suite, Usually how you want to name the desired behavior 24 | * @body Required: A closure that will resemble the tests to execute. 25 | * @labels The list or array of labels this suite group belongs to 26 | * @asyncAll If you want to parallelize the execution of the defined specs in this suite group. 27 | * @skip A flag that tells TestBox to skip this suite group from testing if true 28 | * @focused A flag that tells TestBox to only run this suite and no other 29 | */ 30 | describe( "Alphabet", function() { 31 | 32 | /** 33 | * -------------------------------------------------------------------------- 34 | * Runs before each spec in THIS suite group or nested groups 35 | * -------------------------------------------------------------------------- 36 | */ 37 | beforeEach( function() { 38 | } ); 39 | 40 | /** 41 | * -------------------------------------------------------------------------- 42 | * Runs after each spec in THIS suite group or nested groups 43 | * -------------------------------------------------------------------------- 44 | */ 45 | afterEach( function() { 46 | } ); 47 | 48 | /** 49 | * it() describes a spec to test. Usually the title is prefixed with the suite name to create an expression. 50 | * You can also use the aliases: then() to create fluent chains of human-readable expressions. 51 | * 52 | * Arguments: 53 | * 54 | * @title The title of this spec 55 | * @body The closure that represents the test 56 | * @labels The list or array of labels this spec belongs to 57 | * @skip A flag or a closure that tells TestBox to skip this spec test from testing if true. If this is a closure it must return boolean. 58 | * @data A struct of data you would like to bind into the spec so it can be later passed into the executing body function 59 | * @focused A flag that tells TestBox to only run this spec and no other 60 | */ 61 | it( "simpel", function() { 62 | var sqidsOptions = new Sqids.SqidsOptions(alphabet="0123456789abcdef"); 63 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 64 | 65 | var numbers = [1, 2, 3]; 66 | var id = '489158'; 67 | 68 | expect(sqidsEncoder.encode(numbers)).toBe(id); 69 | expect(sqidsEncoder.decode(id)).toBe(numbers); 70 | } ); 71 | 72 | it( "short alphabet", function() { 73 | var sqidsOptions = new Sqids.SqidsOptions(alphabet="abc"); 74 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 75 | 76 | var numbers = [1, 2, 3]; 77 | 78 | expect(sqidsEncoder.decode(sqidsEncoder.encode(numbers))).toBe(numbers); 79 | } ); 80 | 81 | it( "long alphabet", function() { 82 | var sqidsOptions = new Sqids.SqidsOptions(alphabet="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@##$%^&*()-_+|{}[];:\'""/?.>,<`~"); 83 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 84 | 85 | var numbers = [1, 2, 3]; 86 | 87 | expect(sqidsEncoder.decode(sqidsEncoder.encode(numbers))).toBe(numbers); 88 | } ); 89 | 90 | it( "multibyte characters", function() { 91 | var sqidsOptions = new Sqids.SqidsOptions(alphabet="ë1092"); 92 | 93 | expect(function() { 94 | var sqids = new Sqids.SqidsEncoder(sqidsOptions); 95 | }).toThrow(type="custom", regex="Alphabet cannot contain multibyte characters"); 96 | } ); 97 | 98 | it( "repeating alphabet characters", function() { 99 | var sqidsOptions = new Sqids.SqidsOptions(alphabet="aabcdefg"); 100 | 101 | expect(function() { 102 | var sqids = new Sqids.SqidsEncoder(sqidsOptions); 103 | }).toThrow(type="custom", regex="Alphabet must contain unique characters"); 104 | } ); 105 | 106 | it( "too short of an alphabet", function() { 107 | var sqidsOptions = new Sqids.SqidsOptions(alphabet="ab"); 108 | 109 | expect(function() { 110 | var sqids = new Sqids.SqidsEncoder(sqidsOptions); 111 | }).toThrow(type="custom", regex="Alphabet length must be at least 3"); 112 | } ); 113 | } ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /tests/specs/BlocklistTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | /*********************************** LIFE CYCLE Methods ***********************************/ 4 | 5 | function beforeAll() { 6 | // setup the entire test bundle here 7 | } 8 | 9 | function afterAll() { 10 | // do cleanup here 11 | } 12 | 13 | /*********************************** BDD SUITES ***********************************/ 14 | 15 | function run() { 16 | /** 17 | * describe() starts a suite group of spec tests. It is the main BDD construct. 18 | * You can also use the aliases: story(), feature(), scenario(), given(), when() 19 | * to create fluent chains of human-readable expressions. 20 | * 21 | * Arguments: 22 | * 23 | * @title Required: The title of the suite, Usually how you want to name the desired behavior 24 | * @body Required: A closure that will resemble the tests to execute. 25 | * @labels The list or array of labels this suite group belongs to 26 | * @asyncAll If you want to parallelize the execution of the defined specs in this suite group. 27 | * @skip A flag that tells TestBox to skip this suite group from testing if true 28 | * @focused A flag that tells TestBox to only run this suite and no other 29 | */ 30 | describe( "Alphabet", function() { 31 | 32 | /** 33 | * -------------------------------------------------------------------------- 34 | * Runs before each spec in THIS suite group or nested groups 35 | * -------------------------------------------------------------------------- 36 | */ 37 | beforeEach( function() { 38 | } ); 39 | 40 | /** 41 | * -------------------------------------------------------------------------- 42 | * Runs after each spec in THIS suite group or nested groups 43 | * -------------------------------------------------------------------------- 44 | */ 45 | afterEach( function() { 46 | } ); 47 | 48 | /** 49 | * it() describes a spec to test. Usually the title is prefixed with the suite name to create an expression. 50 | * You can also use the aliases: then() to create fluent chains of human-readable expressions. 51 | * 52 | * Arguments: 53 | * 54 | * @title The title of this spec 55 | * @body The closure that represents the test 56 | * @labels The list or array of labels this spec belongs to 57 | * @skip A flag or a closure that tells TestBox to skip this spec test from testing if true. If this is a closure it must return boolean. 58 | * @data A struct of data you would like to bind into the spec so it can be later passed into the executing body function 59 | * @focused A flag that tells TestBox to only run this spec and no other 60 | */ 61 | it( "if no custom blocklist param, use the default blocklist", function() { 62 | var sqidsOptions = new Sqids.SqidsOptions(); 63 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 64 | 65 | expect(sqidsEncoder.decode('aho1e')).toBe([4572721]); 66 | expect(sqidsEncoder.encode([4572721])).toBe('JExTR'); 67 | } ); 68 | 69 | it( "if an empty blocklist param passed, don't use any blocklist", function() { 70 | var sqidsOptions = new Sqids.SqidsOptions(blocklist=[]); 71 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 72 | 73 | expect(sqidsEncoder.decode('aho1e')).toBe([4572721]); 74 | expect(sqidsEncoder.encode([4572721])).toBe('aho1e'); 75 | } ); 76 | 77 | it( "if a non-empty blocklist param passed, use only that", function() { 78 | var sqidsOptions = new Sqids.SqidsOptions(blocklist=["ArUO"]); // originally encoded [100000] 79 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 80 | 81 | // make sure we don't use the default blocklist 82 | expect(sqidsEncoder.decode('aho1e')).toBe([4572721]); 83 | expect(sqidsEncoder.encode([4572721])).toBe('aho1e'); 84 | 85 | // make sure we are using the passed blocklist 86 | expect(sqidsEncoder.decode('ArUO')).toBe([100000]); 87 | expect(sqidsEncoder.encode([100000])).toBe('QyG4'); 88 | expect(sqidsEncoder.decode('QyG4')).toBe([100000]); 89 | } ); 90 | 91 | it( "blocklist", function() { 92 | var sqidsOptions = new Sqids.SqidsOptions(blocklist = [ 93 | "JSwXFaosAN", // normal result of 1st encoding, let's block that word on purpose 94 | "OCjV9JK64o", // result of 2nd encoding 95 | "rBHf", // result of 3rd encoding is `4rBHfOiqd3`, let's block a substring 96 | "79SM", // result of 4th encoding is `dyhgw479SM`, let's block the postfix 97 | "7tE6" // result of 4th encoding is `7tE6jdAHLe`, let's block the prefix 98 | ]); 99 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 100 | 101 | expect(sqidsEncoder.encode([1000000, 2000000])).toBe('1aYeB7bRUt'); 102 | expect(sqidsEncoder.decode('1aYeB7bRUt')).toBe([1000000, 2000000]); 103 | } ); 104 | 105 | it( "decoding blocklist words should still work", function() { 106 | var sqidsOptions = new Sqids.SqidsOptions(blocklist=["86Rf07", "se8ojk", "ARsz1p", "Q8AI49", "5sQRZO"]); 107 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 108 | 109 | expect(sqidsEncoder.decode('86Rf07')).toBe([1, 2, 3]); 110 | expect(sqidsEncoder.decode('se8ojk')).toBe([1, 2, 3]); 111 | expect(sqidsEncoder.decode('ARsz1p')).toBe([1, 2, 3]); 112 | expect(sqidsEncoder.decode('Q8AI49')).toBe([1, 2, 3]); 113 | expect(sqidsEncoder.decode('5sQRZO')).toBe([1, 2, 3]); 114 | } ); 115 | 116 | it( "match against a short blocklist word", function() { 117 | var sqidsOptions = new Sqids.SqidsOptions(blocklist=["pnd"]); 118 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 119 | 120 | expect(sqidsEncoder.decode(sqidsEncoder.encode([1000]))).toBe([1000]); 121 | } ); 122 | 123 | 124 | it( "blocklist filtering in constructor", function() { 125 | var sqidsEncoder = new Sqids.SqidsEncoder(new Sqids.SqidsOptions( 126 | alphabet="ABCDEFGHIJKLMNOPQRSTUVWXYZ", 127 | blocklist=["sxnzkl"] // lowercase blocklist in only-uppercase alphabet 128 | )); 129 | 130 | var id = sqidsEncoder.encode([1, 2, 3]); 131 | var numbers = sqidsEncoder.decode(id); 132 | 133 | expect(id).toBe('IBSHOZ'); // without blocklist, would've been "SXNZKL" 134 | expect(numbers).toBe([1, 2, 3]); 135 | } ); 136 | 137 | 138 | it( "max encoding attempt", function() { 139 | var sqidsOptions = new Sqids.SqidsOptions(alphabet="abc", minLength=3, blocklist=["cab", "abc", "bca"]); 140 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 141 | 142 | expect(sqidsOptions.getAlphabet().len()).toBe(sqidsOptions.getMinLength()); 143 | expect(sqidsOptions.getBlocklist().len()).toBe(sqidsOptions.getMinLength()); 144 | expect(function() { 145 | var id = sqidsEncoder.encode([0]); 146 | }).toThrow(type = "custom", regex = "Reached max attempts to re-generate the ID"); 147 | } ); 148 | } ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /tests/specs/EncodingTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | /*********************************** LIFE CYCLE Methods ***********************************/ 4 | 5 | function beforeAll() { 6 | // setup the entire test bundle here 7 | variables.SqidsEncoder = new Sqids.SqidsEncoder(); 8 | variables.MaxNumber = createObject("java", "java.lang.Integer").MAX_VALUE; 9 | } 10 | 11 | function afterAll() { 12 | // do cleanup here 13 | } 14 | 15 | /*********************************** BDD SUITES ***********************************/ 16 | 17 | function run() { 18 | /** 19 | * describe() starts a suite group of spec tests. It is the main BDD construct. 20 | * You can also use the aliases: story(), feature(), scenario(), given(), when() 21 | * to create fluent chains of human-readable expressions. 22 | * 23 | * Arguments: 24 | * 25 | * @title Required: The title of the suite, Usually how you want to name the desired behavior 26 | * @body Required: A closure that will resemble the tests to execute. 27 | * @labels The list or array of labels this suite group belongs to 28 | * @asyncAll If you want to parallelize the execution of the defined specs in this suite group. 29 | * @skip A flag that tells TestBox to skip this suite group from testing if true 30 | * @focused A flag that tells TestBox to only run this suite and no other 31 | */ 32 | describe( "Encoding", function() { 33 | 34 | /** 35 | * -------------------------------------------------------------------------- 36 | * Runs before each spec in THIS suite group or nested groups 37 | * -------------------------------------------------------------------------- 38 | */ 39 | beforeEach( function() { 40 | } ); 41 | 42 | /** 43 | * -------------------------------------------------------------------------- 44 | * Runs after each spec in THIS suite group or nested groups 45 | * -------------------------------------------------------------------------- 46 | */ 47 | afterEach( function() { 48 | } ); 49 | 50 | /** 51 | * it() describes a spec to test. Usually the title is prefixed with the suite name to create an expression. 52 | * You can also use the aliases: then() to create fluent chains of human-readable expressions. 53 | * 54 | * Arguments: 55 | * 56 | * @title The title of this spec 57 | * @body The closure that represents the test 58 | * @labels The list or array of labels this spec belongs to 59 | * @skip A flag or a closure that tells TestBox to skip this spec test from testing if true. If this is a closure it must return boolean. 60 | * @data A struct of data you would like to bind into the spec so it can be later passed into the executing body function 61 | * @focused A flag that tells TestBox to only run this spec and no other 62 | */ 63 | it( "just one", function() { 64 | var numbers = [1]; 65 | var id = 'Uk'; 66 | 67 | expect(variables.SqidsEncoder.encode(numbers)).toBe(id); 68 | expect(variables.SqidsEncoder.decode(id)).toBe(numbers); 69 | } ); 70 | 71 | it( "simple", function() { 72 | var numbers = [1, 2, 3]; 73 | var id = '86Rf07'; 74 | 75 | expect(variables.SqidsEncoder.encode(numbers)).toBe(id); 76 | expect(variables.SqidsEncoder.decode(id)).toBe(numbers); 77 | } ); 78 | 79 | it( "different inputs", function() { 80 | var numbers = [0, 0, 0, 1, 2, 3, 100, 1000, 100000, 1000000, variables.MaxNumber]; 81 | 82 | expect(variables.SqidsEncoder.decode(variables.SqidsEncoder.encode(numbers))).toBe(numbers); 83 | } ); 84 | 85 | it( "incremental numbers", function() { 86 | var ids = [ 87 | "bM": [0], 88 | "Uk": [1], 89 | "gb": [2], 90 | "Ef": [3], 91 | "Vq": [4], 92 | "uw": [5], 93 | "OI": [6], 94 | "AX": [7], 95 | "p6": [8], 96 | "nJ": [9] 97 | ]; 98 | 99 | ids.each(function(required string id, required array numbers) { 100 | expect(variables.SqidsEncoder.encode(arguments.numbers)).toBe(arguments.id); 101 | expect(variables.SqidsEncoder.decode(arguments.id)).toBe(arguments.numbers); 102 | } ); 103 | } ); 104 | 105 | it( "incremental numbers, same index 0", function() { 106 | var ids = [ 107 | "SvIz": [0, 0], 108 | "n3qa": [0, 1], 109 | "tryF": [0, 2], 110 | "eg6q": [0, 3], 111 | "rSCF": [0, 4], 112 | "sR8x": [0, 5], 113 | "uY2M": [0, 6], 114 | "74dI": [0, 7], 115 | "30WX": [0, 8], 116 | "moxr": [0, 9] 117 | ]; 118 | 119 | ids.each(function(required string id, required array numbers) { 120 | expect(variables.SqidsEncoder.encode(arguments.numbers)).toBe(arguments.id); 121 | expect(variables.SqidsEncoder.decode(arguments.id)).toBe(arguments.numbers); 122 | } ); 123 | } ); 124 | 125 | it( "incremental numbers, same index 1", function() { 126 | var ids = [ 127 | "SvIz": [0, 0], 128 | "nWqP": [1, 0], 129 | "tSyw": [2, 0], 130 | "eX68": [3, 0], 131 | "rxCY": [4, 0], 132 | "sV8a": [5, 0], 133 | "uf2K": [6, 0], 134 | "7Cdk": [7, 0], 135 | "3aWP": [8, 0], 136 | "m2xn": [9, 0] 137 | ]; 138 | 139 | ids.each(function(required string id, required array numbers) { 140 | expect(variables.SqidsEncoder.encode(arguments.numbers)).toBe(arguments.id); 141 | expect(variables.SqidsEncoder.decode(arguments.id)).toBe(arguments.numbers); 142 | } ); 143 | } ); 144 | 145 | it( "multi input", function() { 146 | var numbers = [ 147 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 148 | 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 149 | 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 150 | 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 151 | 98, 99 152 | ]; 153 | 154 | var output = variables.SqidsEncoder.decode(variables.SqidsEncoder.encode(numbers)); 155 | expect(output).toBe(numbers); 156 | } ); 157 | 158 | it( "encoding no numbers", function() { 159 | expect(variables.SqidsEncoder.encode([])).toBe(""); 160 | } ); 161 | 162 | it( "decoding empty string", function() { 163 | expect(variables.SqidsEncoder.decode("")).toBe([]); 164 | } ); 165 | 166 | it( "decoding an ID with an invalid character", function() { 167 | expect(variables.SqidsEncoder.decode("*")).toBe([]); 168 | } ); 169 | 170 | it( "encode out-of-range numbers", function() { 171 | expect(function() { 172 | variables.SqidsEncoder.encode([-1]); 173 | }).toThrow(type = "custom", regex = "Encoding supports numbers between 0 and #variables.MaxNumber#"); 174 | expect(function() { 175 | variables.SqidsEncoder.encode([variables.MaxNumber + 1]); 176 | }).toThrow(type = "custom", regex = "Encoding supports numbers between 0 and #variables.MaxNumber#"); 177 | } ); 178 | } ); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/specs/MinLengthTest.cfc: -------------------------------------------------------------------------------- 1 | component extends="testbox.system.BaseSpec" { 2 | 3 | /*********************************** LIFE CYCLE Methods ***********************************/ 4 | 5 | function beforeAll() { 6 | // setup the entire test bundle here 7 | var sqidsOptions = new Sqids.SqidsOptions(); 8 | variables.defaultAlphabetLength = sqidsOptions.getAlphabet().len(); 9 | variables.maxNumber = createObject("java", "java.lang.Integer").MAX_VALUE; 10 | } 11 | 12 | function afterAll() { 13 | // do cleanup here 14 | } 15 | 16 | /*********************************** BDD SUITES ***********************************/ 17 | 18 | function run() { 19 | /** 20 | * describe() starts a suite group of spec tests. It is the main BDD construct. 21 | * You can also use the aliases: story(), feature(), scenario(), given(), when() 22 | * to create fluent chains of human-readable expressions. 23 | * 24 | * Arguments: 25 | * 26 | * @title Required: The title of the suite, Usually how you want to name the desired behavior 27 | * @body Required: A closure that will resemble the tests to execute. 28 | * @labels The list or array of labels this suite group belongs to 29 | * @asyncAll If you want to parallelize the execution of the defined specs in this suite group. 30 | * @skip A flag that tells TestBox to skip this suite group from testing if true 31 | * @focused A flag that tells TestBox to only run this suite and no other 32 | */ 33 | describe( "Alphabet", function() { 34 | 35 | /** 36 | * -------------------------------------------------------------------------- 37 | * Runs before each spec in THIS suite group or nested groups 38 | * -------------------------------------------------------------------------- 39 | */ 40 | beforeEach( function() { 41 | } ); 42 | 43 | /** 44 | * -------------------------------------------------------------------------- 45 | * Runs after each spec in THIS suite group or nested groups 46 | * -------------------------------------------------------------------------- 47 | */ 48 | afterEach( function() { 49 | } ); 50 | 51 | /** 52 | * it() describes a spec to test. Usually the title is prefixed with the suite name to create an expression. 53 | * You can also use the aliases: then() to create fluent chains of human-readable expressions. 54 | * 55 | * Arguments: 56 | * 57 | * @title The title of this spec 58 | * @body The closure that represents the test 59 | * @labels The list or array of labels this spec belongs to 60 | * @skip A flag or a closure that tells TestBox to skip this spec test from testing if true. If this is a closure it must return boolean. 61 | * @data A struct of data you would like to bind into the spec so it can be later passed into the executing body function 62 | * @focused A flag that tells TestBox to only run this spec and no other 63 | */ 64 | it( "simple", function() { 65 | var sqidsOptions = new Sqids.SqidsOptions(minLength=variables.defaultAlphabetLength); 66 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 67 | 68 | var numbers = [1, 2, 3]; 69 | var id = '86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM'; 70 | 71 | expect(sqidsEncoder.encode(numbers)).toBe(id); 72 | expect(sqidsEncoder.decode(id)).toBe(numbers); 73 | } ); 74 | 75 | it( "incremental", function() { 76 | var numbers = [1, 2, 3]; 77 | 78 | var map = [ 79 | "6": "86Rf07", 80 | "7": "86Rf07x", 81 | "8": "86Rf07xd", 82 | "9": "86Rf07xd4", 83 | "10": "86Rf07xd4z", 84 | "11": "86Rf07xd4zB", 85 | "12": "86Rf07xd4zBm", 86 | "13": "86Rf07xd4zBmi", 87 | "#variables.defaultAlphabetLength + 0#": 88 | "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTM", 89 | "#variables.defaultAlphabetLength + 1#": 90 | "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMy", 91 | "#variables.defaultAlphabetLength + 2#": 92 | "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf", 93 | "#variables.defaultAlphabetLength + 3#": 94 | "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf1", 95 | "256": 96 | "86Rf07xd4zBmiJXQG6otHEbew02c3PWsUOLZxADhCpKj7aVFv9I8RquYrNlSTMyf15kngGjqB9KxuSsQ1tvrCU6LOgdeEoXAInmPHfaJVkWYi2FzyNR7MpDZ4l83wh0Tb5cGgOrESNkXR8I6Uxeqn2pidKJLtZyDQPwWubBAVT5zfo01Mh3sHCY79j4lFvmacL6NH5VZTd3BkWPRO7EnK8IxrQwUoMq2SACJipDu10s4jaYGehFXytgbfzml9vcy" 97 | ]; 98 | 99 | map.each(function(required numeric minLength, required string id) { 100 | var sqidsOptions = new Sqids.SqidsOptions(minLength = arguments.minLength); 101 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 102 | 103 | expect(sqidsEncoder.encode(numbers)).toBe(arguments.id); 104 | expect(sqidsEncoder.encode(numbers).len()).toBe(arguments.minLength); 105 | expect(sqidsEncoder.decode(arguments.id)).toBe(numbers); 106 | }); 107 | 108 | } ); 109 | 110 | it( "incremental numbers", function() { 111 | var sqidsOptions = new Sqids.SqidsOptions(minLength = variables.defaultAlphabetLength); 112 | var sqidsEncoder = new Sqids.SqidsEncoder(sqidsOptions); 113 | 114 | var ids = [ 115 | "SvIzsqYMyQwI3GWgJAe17URxX8V924Co0DaTZLtFjHriEn5bPhcSkfmvOslpBu": [0, 0], 116 | "n3qafPOLKdfHpuNw3M61r95svbeJGk7aAEgYn4WlSjXURmF8IDqZBy0CT2VxQc": [0, 1], 117 | "tryFJbWcFMiYPg8sASm51uIV93GXTnvRzyfLleh06CpodJD42B7OraKtkQNxUZ": [0, 2], 118 | "eg6ql0A3XmvPoCzMlB6DraNGcWSIy5VR8iYup2Qk4tjZFKe1hbwfgHdUTsnLqE": [0, 3], 119 | "rSCFlp0rB2inEljaRdxKt7FkIbODSf8wYgTsZM1HL9JzN35cyoqueUvVWCm4hX": [0, 4], 120 | "sR8xjC8WQkOwo74PnglH1YFdTI0eaf56RGVSitzbjuZ3shNUXBrqLxEJyAmKv2": [0, 5], 121 | "uY2MYFqCLpgx5XQcjdtZK286AwWV7IBGEfuS9yTmbJvkzoUPeYRHr4iDs3naN0": [0, 6], 122 | "74dID7X28VLQhBlnGmjZrec5wTA1fqpWtK4YkaoEIM9SRNiC3gUJH0OFvsPDdy": [0, 7], 123 | "30WXpesPhgKiEI5RHTY7xbB1GnytJvXOl2p0AcUjdF6waZDo9Qk8VLzMuWrqCS": [0, 8], 124 | "moxr3HqLAK0GsTND6jowfZz3SUx7cQ8aC54Pl1RbIvFXmEJuBMYVeW9yrdOtin": [0, 9] 125 | ]; 126 | 127 | ids.each(function(required string id, required array numbers) { 128 | expect(sqidsEncoder.encode(arguments.numbers)).toBe(arguments.id); 129 | expect(sqidsEncoder.decode(arguments.id)).toBe(arguments.numbers); 130 | }); 131 | } ); 132 | 133 | it( "min lengths", function() { 134 | for (var minLength in [0, 1, 5, 10, variables.defaultAlphabetLength]) { 135 | var sqidsEncoder = new Sqids.SqidsEncoder(new Sqids.SqidsOptions(minLength=minLength)); 136 | for (var numbers in [ 137 | [0], 138 | [0, 0, 0, 0, 0], 139 | [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 140 | [100, 200, 300], 141 | [1000, 2000, 3000], 142 | [1000000], 143 | [variables.maxNumber] 144 | ]) { 145 | var id = sqidsEncoder.encode(numbers); 146 | expect(id.len()).toBeGTE(minLength); 147 | expect(sqidsEncoder.decode(id)).toBe(numbers); 148 | } 149 | } 150 | } ); 151 | } ); 152 | } 153 | } 154 | --------------------------------------------------------------------------------