├── .github └── pull_request_template.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── base32 ├── base32.go └── base32_test.go ├── bench_test.go ├── constructors.go ├── doc.go ├── encoding.go ├── encoding_test.go ├── go.mod ├── go.sum ├── prefix.go ├── sql.go ├── sql_test.go ├── subtype.go ├── subtype_example_test.go ├── subtype_test.go ├── testdata ├── README.md ├── invalid.yml └── valid.yml ├── typeid.go ├── typeid_test.go └── validate.go /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # ⚠️ Submit PRs in our opensource monorepo instead ⚠️ 2 | 3 | This repository is automatically published from our opensource monorepo: 4 | https://github.com/jetify-com/opensource 5 | 6 | If you want to contribute code changes to this project, please submit your 7 | PR via the monorepo. 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Benchmark results 2 | benchmark_results/ 3 | 4 | # Go specific 5 | *.prof 6 | *.test 7 | *.out 8 | /vendor/ 9 | 10 | # OS specific 11 | .DS_Store 12 | Thumbs.db 13 | 14 | # Editor files 15 | .idea/ 16 | .vscode/ 17 | *.swp 18 | *.swo 19 | *~ 20 | 21 | # Build artifacts 22 | bin/ 23 | dist/ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement. Use the 63 | "Report to repository admins" functionality on GitHub to report. 64 | 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [mozilla coc]: https://github.com/mozilla/diversity 132 | [faq]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please describe the change you wish to 4 | make via a related issue, or a pull request. 5 | 6 | Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in 7 | all your interactions with the project. 8 | 9 | ## Opening a Pull Request 10 | 11 | This project is published as a standalone repo from our 12 | [opensource monorepo](https://github.com/jetify-com/opensource). Pull requests 13 | should be sent to the monorepo instead, and they will automatically be published 14 | to this repo when merged. 15 | 16 | Contributions made to this project must be made under the terms of the 17 | [Apache 2 License](https://www.apache.org/licenses/LICENSE-2.0). 18 | By contributing to this project you agree to the terms stated in the 19 | [Community Contribution License](https://github.com/jetify-com/opensource/blob/main/CONTRIBUTING.md#community-contribution-license). 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeID Go 2 | 3 | ### A golang implementation of [TypeIDs](https://github.com/jetify-com/typeid) 4 | 5 | ![License: Apache 2.0](https://img.shields.io/github/license/jetify-com/typeid-go) [![Go Reference](https://pkg.go.dev/badge/go.jetify.com/typeid.svg)](https://pkg.go.dev/go.jetify.com/typeid) 6 | 7 | TypeIDs are a modern, **type-safe**, globally unique identifier based on the upcoming 8 | UUIDv7 standard. They provide a ton of nice properties that make them a great choice 9 | as the primary identifiers for your data in a database, APIs, and distributed systems. 10 | Read more about TypeIDs in their [spec](https://github.com/jetify-com/typeid). 11 | 12 | This particular implementation provides a go library for generating and parsing TypeIDs. 13 | 14 | ## Installation 15 | 16 | To add this library as a dependency in your go module, run: 17 | 18 | ```bash 19 | go get go.jetify.com/typeid 20 | ``` 21 | 22 | ## Usage 23 | 24 | This library provides a go implementation of TypeID that allows you 25 | to define your own custom id types for added compile-time safety. 26 | 27 | If you don't need compile-time safety, you can use the provided `typeid.AnyID` directly: 28 | 29 | ```go 30 | import ( 31 | "go.jetify.com/typeid" 32 | ) 33 | 34 | func example() { 35 | tid, _ := typeid.WithPrefix("user") 36 | fmt.Println(tid) 37 | } 38 | ``` 39 | 40 | If you want compile-time safety, define your own custom types with two steps: 41 | 42 | 1. Define a struct the implements the method `Prefix`. Prefix should return the 43 | string that should be used as the prefix for your custom type. 44 | 2. Define you own id type, by embedding `typeid.TypeID[CustomPrefix]` 45 | 46 | For example to define a UserID with prefix `user`: 47 | 48 | ```go 49 | import ( 50 | "go.jetify.com/typeid" 51 | ) 52 | 53 | // Define the prefix: 54 | type UserPrefix struct {} 55 | func (UserPrefix) Prefix() string { return "user" } 56 | 57 | // Define UserID: 58 | type UserID struct { 59 | typeid.TypeID[UserPrefix] 60 | } 61 | ``` 62 | 63 | Now you can use the UserID type to generate new ids: 64 | 65 | ```go 66 | import ( 67 | "go.jetify.com/typeid" 68 | ) 69 | 70 | func example() { 71 | tid, _ := typeid.New[UserID]() 72 | fmt.Println(tid) 73 | } 74 | ``` 75 | 76 | For the full documentation, see this package's [godoc](https://pkg.go.dev/go.jetify.com/typeid). 77 | -------------------------------------------------------------------------------- /base32/base32.go: -------------------------------------------------------------------------------- 1 | package base32 2 | 3 | // Encoding and Decoding code based on the go implementation of ulid 4 | // found at: https://github.com/oklog/ulid 5 | // (Copyright 2016 The Oklog Authors) 6 | // Modifications made available under the same license as the original 7 | 8 | import ( 9 | "errors" 10 | ) 11 | 12 | const alphabet = "0123456789abcdefghjkmnpqrstvwxyz" 13 | 14 | func Encode(src [16]byte) string { 15 | dst := make([]byte, 26) 16 | // Optimized unrolled loop ahead. 17 | 18 | // 10 byte timestamp 19 | dst[0] = alphabet[(src[0]&224)>>5] 20 | dst[1] = alphabet[src[0]&31] 21 | dst[2] = alphabet[(src[1]&248)>>3] 22 | dst[3] = alphabet[((src[1]&7)<<2)|((src[2]&192)>>6)] 23 | dst[4] = alphabet[(src[2]&62)>>1] 24 | dst[5] = alphabet[((src[2]&1)<<4)|((src[3]&240)>>4)] 25 | dst[6] = alphabet[((src[3]&15)<<1)|((src[4]&128)>>7)] 26 | dst[7] = alphabet[(src[4]&124)>>2] 27 | dst[8] = alphabet[((src[4]&3)<<3)|((src[5]&224)>>5)] 28 | dst[9] = alphabet[src[5]&31] 29 | 30 | // 16 bytes of entropy 31 | dst[10] = alphabet[(src[6]&248)>>3] 32 | dst[11] = alphabet[((src[6]&7)<<2)|((src[7]&192)>>6)] 33 | dst[12] = alphabet[(src[7]&62)>>1] 34 | dst[13] = alphabet[((src[7]&1)<<4)|((src[8]&240)>>4)] 35 | dst[14] = alphabet[((src[8]&15)<<1)|((src[9]&128)>>7)] 36 | dst[15] = alphabet[(src[9]&124)>>2] 37 | dst[16] = alphabet[((src[9]&3)<<3)|((src[10]&224)>>5)] 38 | dst[17] = alphabet[src[10]&31] 39 | dst[18] = alphabet[(src[11]&248)>>3] 40 | dst[19] = alphabet[((src[11]&7)<<2)|((src[12]&192)>>6)] 41 | dst[20] = alphabet[(src[12]&62)>>1] 42 | dst[21] = alphabet[((src[12]&1)<<4)|((src[13]&240)>>4)] 43 | dst[22] = alphabet[((src[13]&15)<<1)|((src[14]&128)>>7)] 44 | dst[23] = alphabet[(src[14]&124)>>2] 45 | dst[24] = alphabet[((src[14]&3)<<3)|((src[15]&224)>>5)] 46 | dst[25] = alphabet[src[15]&31] 47 | 48 | return string(dst) 49 | } 50 | 51 | // Byte to index table for O(1) lookups when unmarshaling. 52 | // We use 0xFF as sentinel value for invalid indexes. 53 | var dec = [...]byte{ 54 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 55 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 56 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 57 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 58 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 59 | 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0xFF, 0xFF, 60 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 61 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 62 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 63 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x0A, 0x0B, 0x0C, 64 | 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0xFF, 0x12, 0x13, 0xFF, 0x14, 65 | 0x15, 0xFF, 0x16, 0x17, 0x18, 0x19, 0x1A, 0xFF, 0x1B, 0x1C, 66 | 0x1D, 0x1E, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 67 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 68 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 69 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 70 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 71 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 72 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 73 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 74 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 75 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 76 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 77 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 78 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 79 | 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 80 | } 81 | 82 | func Decode(s string) ([]byte, error) { 83 | if len(s) != 26 { 84 | return nil, errors.New("invalid length") 85 | } 86 | 87 | val := []byte(s) 88 | // Check if all the characters are part of the expected base32 character set. 89 | if dec[val[0]] == 0xFF || 90 | dec[val[1]] == 0xFF || 91 | dec[val[2]] == 0xFF || 92 | dec[val[3]] == 0xFF || 93 | dec[val[4]] == 0xFF || 94 | dec[val[5]] == 0xFF || 95 | dec[val[6]] == 0xFF || 96 | dec[val[7]] == 0xFF || 97 | dec[val[8]] == 0xFF || 98 | dec[val[9]] == 0xFF || 99 | dec[val[10]] == 0xFF || 100 | dec[val[11]] == 0xFF || 101 | dec[val[12]] == 0xFF || 102 | dec[val[13]] == 0xFF || 103 | dec[val[14]] == 0xFF || 104 | dec[val[15]] == 0xFF || 105 | dec[val[16]] == 0xFF || 106 | dec[val[17]] == 0xFF || 107 | dec[val[18]] == 0xFF || 108 | dec[val[19]] == 0xFF || 109 | dec[val[20]] == 0xFF || 110 | dec[val[21]] == 0xFF || 111 | dec[val[22]] == 0xFF || 112 | dec[val[23]] == 0xFF || 113 | dec[val[24]] == 0xFF || 114 | dec[val[25]] == 0xFF { 115 | return nil, errors.New("invalid base32 character") 116 | } 117 | 118 | id := make([]byte, 16) 119 | 120 | // 6 bytes timestamp (48 bits) 121 | id[0] = (dec[val[0]] << 5) | dec[val[1]] 122 | id[1] = (dec[val[2]] << 3) | (dec[val[3]] >> 2) 123 | id[2] = (dec[val[3]] << 6) | (dec[val[4]] << 1) | (dec[val[5]] >> 4) 124 | id[3] = (dec[val[5]] << 4) | (dec[val[6]] >> 1) 125 | id[4] = (dec[val[6]] << 7) | (dec[val[7]] << 2) | (dec[val[8]] >> 3) 126 | id[5] = (dec[val[8]] << 5) | dec[val[9]] 127 | 128 | // 10 bytes of entropy (80 bits) 129 | id[6] = (dec[val[10]] << 3) | (dec[val[11]] >> 2) // First 4 bits are the version 130 | id[7] = (dec[val[11]] << 6) | (dec[val[12]] << 1) | (dec[val[13]] >> 4) 131 | id[8] = (dec[val[13]] << 4) | (dec[val[14]] >> 1) // First 2 bits are the variant 132 | id[9] = (dec[val[14]] << 7) | (dec[val[15]] << 2) | (dec[val[16]] >> 3) 133 | id[10] = (dec[val[16]] << 5) | dec[val[17]] 134 | id[11] = (dec[val[18]] << 3) | dec[val[19]]>>2 135 | id[12] = (dec[val[19]] << 6) | (dec[val[20]] << 1) | (dec[val[21]] >> 4) 136 | id[13] = (dec[val[21]] << 4) | (dec[val[22]] >> 1) 137 | id[14] = (dec[val[22]] << 7) | (dec[val[23]] << 2) | (dec[val[24]] >> 3) 138 | id[15] = (dec[val[24]] << 5) | dec[val[25]] 139 | 140 | return id, nil 141 | } 142 | -------------------------------------------------------------------------------- /base32/base32_test.go: -------------------------------------------------------------------------------- 1 | package base32 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base32" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestEncodeDecode(t *testing.T) { 12 | encoder := base32.NewEncoding("0123456789abcdefghjkmnpqrstvwxyz") 13 | 14 | for i := 0; i < 1000; i++ { 15 | // Generate 16 random bytes 16 | data := make([]byte, 16) 17 | _, err := rand.Read(data) 18 | assert.NoError(t, err) 19 | 20 | // Encode them using our library, and encode them using go's standard library: 21 | actual := Encode([16]byte(data)) 22 | 23 | // The standard base32 library decodes in groups of 5 bytes, otherwise it needs 24 | // to pad, by default it pads at the end of the byte array, but to match our 25 | // encoding we need to pad in the front. 26 | // Pad manually, and then remove the extra 000000 from the resulting string. 27 | padded := append([]byte{0x00, 0x00, 0x00, 0x00}, data...) 28 | expected := encoder.EncodeToString(padded)[6:] 29 | 30 | // They should be equal 31 | assert.Equal(t, expected, actual) 32 | 33 | // Decoding again should yield the original result: 34 | decoded, err := Decode(actual) 35 | assert.NoError(t, err) 36 | for i := 0; i < 16; i++ { 37 | assert.Equal(t, data[i], decoded[i]) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | //nolint:all 2 | package typeid_test 3 | 4 | import ( 5 | "fmt" 6 | "math/rand" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/gofrs/uuid/v5" 11 | "go.jetify.com/typeid" 12 | "go.jetify.com/typeid/base32" 13 | ) 14 | 15 | func BenchmarkNew(b *testing.B) { 16 | b.Run("id=untyped", func(b *testing.B) { 17 | b.ReportAllocs() 18 | for i := 0; i < b.N; i++ { 19 | typeid.WithPrefix("prefix") 20 | } 21 | }) 22 | b.Run("id=typed", func(b *testing.B) { 23 | b.ReportAllocs() 24 | for i := 0; i < b.N; i++ { 25 | typeid.New[TestID]() 26 | } 27 | }) 28 | b.Run("id=uuid", func(b *testing.B) { 29 | b.ReportAllocs() 30 | for i := 0; i < b.N; i++ { 31 | uuid.NewV7() 32 | } 33 | }) 34 | // Add benchmark for different prefix lengths 35 | b.Run("prefix=short", func(b *testing.B) { 36 | b.ReportAllocs() 37 | for i := 0; i < b.N; i++ { 38 | typeid.WithPrefix("s") 39 | } 40 | }) 41 | b.Run("prefix=medium", func(b *testing.B) { 42 | b.ReportAllocs() 43 | for i := 0; i < b.N; i++ { 44 | typeid.WithPrefix("medium") 45 | } 46 | }) 47 | b.Run("prefix=long", func(b *testing.B) { 48 | b.ReportAllocs() 49 | for i := 0; i < b.N; i++ { 50 | typeid.WithPrefix("thisislongprefix") 51 | } 52 | }) 53 | } 54 | 55 | func BenchmarkString(b *testing.B) { 56 | b.Run("id=untyped", func(b *testing.B) { 57 | b.Run(benchUntypedString(1)) 58 | b.Run(benchUntypedString(8)) 59 | b.Run(benchUntypedString(64)) 60 | b.Run(benchUntypedString(4096)) 61 | }) 62 | b.Run("id=typed", func(b *testing.B) { 63 | b.Run(benchTypedString(1)) 64 | b.Run(benchTypedString(8)) 65 | b.Run(benchTypedString(64)) 66 | b.Run(benchTypedString(4096)) 67 | }) 68 | b.Run("id=uuid", func(b *testing.B) { 69 | b.Run(benchUUIDString(1)) 70 | b.Run(benchUUIDString(8)) 71 | b.Run(benchUUIDString(64)) 72 | b.Run(benchUUIDString(4096)) 73 | }) 74 | } 75 | 76 | func benchUntypedString(n int) (string, func(*testing.B)) { 77 | ids := make([]typeid.AnyID, n) 78 | for i := range ids { 79 | ids[i] = typeid.Must(typeid.WithPrefix("prefix")) 80 | } 81 | return fmt.Sprintf("n=%d", n), func(b *testing.B) { 82 | b.ReportAllocs() 83 | for i := 0; i < b.N; i++ { 84 | for _, id := range ids { 85 | _ = id.String() 86 | } 87 | } 88 | b.ReportMetric(float64(n*b.N)/b.Elapsed().Seconds(), "id/s") 89 | } 90 | } 91 | 92 | func benchTypedString(n int) (string, func(*testing.B)) { 93 | ids := make([]TestID, n) 94 | for i := range ids { 95 | ids[i] = typeid.Must(typeid.New[TestID]()) 96 | } 97 | return fmt.Sprintf("n=%d", n), func(b *testing.B) { 98 | b.ReportAllocs() 99 | for i := 0; i < b.N; i++ { 100 | for _, id := range ids { 101 | _ = id.String() 102 | } 103 | } 104 | b.ReportMetric(float64(n*b.N)/b.Elapsed().Seconds(), "id/s") 105 | } 106 | } 107 | 108 | func benchUUIDString(n int) (string, func(*testing.B)) { 109 | uuids := make([]uuid.UUID, n) 110 | for i := range uuids { 111 | uuids[i] = uuid.Must(uuid.NewV7()) 112 | } 113 | return fmt.Sprintf("n=%d", n), func(b *testing.B) { 114 | b.ReportAllocs() 115 | for i := 0; i < b.N; i++ { 116 | for _, id := range uuids { 117 | _ = id.String() 118 | } 119 | } 120 | b.ReportMetric(float64(n*b.N)/b.Elapsed().Seconds(), "id/s") 121 | } 122 | } 123 | 124 | func BenchmarkFrom(b *testing.B) { 125 | b.Run("id=untyped", func(b *testing.B) { 126 | b.Run(benchUntypedFrom(1)) 127 | b.Run(benchUntypedFrom(8)) 128 | b.Run(benchUntypedFrom(64)) 129 | b.Run(benchUntypedFrom(4096)) 130 | }) 131 | b.Run("id=typed", func(b *testing.B) { 132 | b.Run(benchTypedFrom(1)) 133 | b.Run(benchTypedFrom(8)) 134 | b.Run(benchTypedFrom(64)) 135 | b.Run(benchTypedFrom(4096)) 136 | }) 137 | } 138 | 139 | func benchUntypedFrom(n int) (string, func(*testing.B)) { 140 | ids := make([]struct{ prefix, suffix string }, n) 141 | for i := range ids { 142 | id := typeid.Must(typeid.WithPrefix("prefix")) 143 | ids[i].prefix, ids[i].suffix = id.Prefix(), id.Suffix() 144 | } 145 | return fmt.Sprintf("n=%d", n), func(b *testing.B) { 146 | b.ReportAllocs() 147 | for i := 0; i < b.N; i++ { 148 | for _, id := range ids { 149 | typeid.From(id.prefix, id.suffix) 150 | } 151 | } 152 | b.ReportMetric(float64(n*b.N)/b.Elapsed().Seconds(), "id/s") 153 | } 154 | } 155 | 156 | func benchTypedFrom(n int) (string, func(*testing.B)) { 157 | suffixes := make([]string, n) 158 | for i := range suffixes { 159 | suffixes[i] = typeid.Must(typeid.New[TestID]()).Suffix() 160 | } 161 | return fmt.Sprintf("n=%d", n), func(b *testing.B) { 162 | b.ReportAllocs() 163 | for i := 0; i < b.N; i++ { 164 | for _, suffix := range suffixes { 165 | typeid.FromSuffix[TestID](suffix) 166 | } 167 | } 168 | b.ReportMetric(float64(n*b.N)/b.Elapsed().Seconds(), "id/s") 169 | } 170 | } 171 | 172 | func BenchmarkFromString(b *testing.B) { 173 | b.Run("id=untyped", func(b *testing.B) { 174 | b.Run(benchUntypedFromString(1)) 175 | b.Run(benchUntypedFromString(8)) 176 | b.Run(benchUntypedFromString(64)) 177 | b.Run(benchUntypedFromString(4096)) 178 | }) 179 | b.Run("id=typed", func(b *testing.B) { 180 | b.Run(benchTypedFromString(1)) 181 | b.Run(benchTypedFromString(8)) 182 | b.Run(benchTypedFromString(64)) 183 | b.Run(benchTypedFromString(4096)) 184 | }) 185 | b.Run("id=uuid", func(b *testing.B) { 186 | b.Run(benchUUIDFromString(1)) 187 | b.Run(benchUUIDFromString(8)) 188 | b.Run(benchUUIDFromString(64)) 189 | b.Run(benchUUIDFromString(4096)) 190 | }) 191 | } 192 | 193 | func benchUntypedFromString(n int) (string, func(*testing.B)) { 194 | ids := make([]string, n) 195 | for i := range ids { 196 | ids[i] = typeid.Must(typeid.WithPrefix("prefix")).String() 197 | } 198 | return fmt.Sprintf("n=%d", n), func(b *testing.B) { 199 | b.ReportAllocs() 200 | for i := 0; i < b.N; i++ { 201 | for _, id := range ids { 202 | typeid.FromString(id) 203 | } 204 | } 205 | b.ReportMetric(float64(n*b.N)/b.Elapsed().Seconds(), "id/s") 206 | } 207 | } 208 | 209 | func benchTypedFromString(n int) (string, func(*testing.B)) { 210 | ids := make([]string, n) 211 | for i := range ids { 212 | ids[i] = typeid.Must(typeid.New[TestID]()).String() 213 | } 214 | return fmt.Sprintf("n=%d", n), func(b *testing.B) { 215 | b.ReportAllocs() 216 | for i := 0; i < b.N; i++ { 217 | for _, id := range ids { 218 | typeid.Parse[TestID](id) 219 | } 220 | } 221 | b.ReportMetric(float64(n*b.N)/b.Elapsed().Seconds(), "id/s") 222 | } 223 | } 224 | 225 | func benchUUIDFromString(n int) (string, func(*testing.B)) { 226 | uuids := make([]string, n) 227 | for i := range uuids { 228 | uuids[i] = uuid.Must(uuid.NewV7()).String() 229 | } 230 | return fmt.Sprintf("n=%d", n), func(b *testing.B) { 231 | b.ReportAllocs() 232 | for i := 0; i < b.N; i++ { 233 | for _, id := range uuids { 234 | uuid.FromString(id) 235 | } 236 | } 237 | b.ReportMetric(float64(n*b.N)/b.Elapsed().Seconds(), "id/s") 238 | } 239 | } 240 | 241 | func BenchmarkFromBytes(b *testing.B) { 242 | b.Run("id=untyped", func(b *testing.B) { 243 | b.Run(benchUntypedFromBytes(1)) 244 | b.Run(benchUntypedFromBytes(8)) 245 | b.Run(benchUntypedFromBytes(64)) 246 | b.Run(benchUntypedFromBytes(4096)) 247 | }) 248 | b.Run("id=typed", func(b *testing.B) { 249 | b.Run(benchTypedFromBytes(1)) 250 | b.Run(benchTypedFromBytes(8)) 251 | b.Run(benchTypedFromBytes(64)) 252 | b.Run(benchTypedFromBytes(4096)) 253 | }) 254 | b.Run("id=uuid", func(b *testing.B) { 255 | b.Run(benchUUIDFromBytes(1)) 256 | b.Run(benchUUIDFromBytes(8)) 257 | b.Run(benchUUIDFromBytes(64)) 258 | b.Run(benchUUIDFromBytes(4096)) 259 | }) 260 | } 261 | 262 | func benchUntypedFromBytes(n int) (string, func(*testing.B)) { 263 | ids := make([][]byte, n) 264 | for i := range ids { 265 | ids[i] = typeid.Must(typeid.WithPrefix("prefix")).UUIDBytes() 266 | } 267 | return fmt.Sprintf("n=%d", n), func(b *testing.B) { 268 | b.ReportAllocs() 269 | for i := 0; i < b.N; i++ { 270 | for _, id := range ids { 271 | typeid.FromUUIDBytesWithPrefix("prefix", id) 272 | } 273 | } 274 | b.ReportMetric(float64(n*b.N)/b.Elapsed().Seconds(), "id/s") 275 | } 276 | } 277 | 278 | func benchTypedFromBytes(n int) (string, func(*testing.B)) { 279 | ids := make([][]byte, n) 280 | for i := range ids { 281 | ids[i] = typeid.Must(typeid.New[TestID]()).UUIDBytes() 282 | } 283 | return fmt.Sprintf("n=%d", n), func(b *testing.B) { 284 | b.ReportAllocs() 285 | for i := 0; i < b.N; i++ { 286 | for _, id := range ids { 287 | typeid.FromUUIDBytesWithPrefix("prefix", id) 288 | } 289 | } 290 | b.ReportMetric(float64(n*b.N)/b.Elapsed().Seconds(), "id/s") 291 | } 292 | } 293 | 294 | func benchUUIDFromBytes(n int) (string, func(*testing.B)) { 295 | uuids := make([][]byte, n) 296 | for i := range uuids { 297 | uuids[i] = uuid.Must(uuid.NewV7()).Bytes() 298 | } 299 | return fmt.Sprintf("n=%d", n), func(b *testing.B) { 300 | b.ReportAllocs() 301 | for i := 0; i < b.N; i++ { 302 | for _, id := range uuids { 303 | uuid.FromBytes(id) 304 | } 305 | } 306 | b.ReportMetric(float64(n*b.N)/b.Elapsed().Seconds(), "id/s") 307 | } 308 | } 309 | 310 | func BenchmarkSuffix(b *testing.B) { 311 | b.Run("id=untyped", func(b *testing.B) { 312 | id := typeid.Must(typeid.WithPrefix("prefix")) 313 | 314 | b.ReportAllocs() 315 | b.ResetTimer() 316 | for i := 0; i < b.N; i++ { 317 | id.Suffix() 318 | } 319 | }) 320 | b.Run("id=typed", func(b *testing.B) { 321 | id := typeid.Must(typeid.New[TestID]()) 322 | 323 | b.ReportAllocs() 324 | b.ResetTimer() 325 | for i := 0; i < b.N; i++ { 326 | id.Suffix() 327 | } 328 | }) 329 | } 330 | 331 | func BenchmarkUUIDBytes(b *testing.B) { 332 | b.Run("id=untyped", func(b *testing.B) { 333 | id := typeid.Must(typeid.WithPrefix("prefix")) 334 | 335 | b.ReportAllocs() 336 | b.ResetTimer() 337 | for i := 0; i < b.N; i++ { 338 | id.UUIDBytes() 339 | } 340 | }) 341 | b.Run("id=typed", func(b *testing.B) { 342 | id := typeid.Must(typeid.New[TestID]()) 343 | 344 | b.ReportAllocs() 345 | b.ResetTimer() 346 | for i := 0; i < b.N; i++ { 347 | id.UUIDBytes() 348 | } 349 | }) 350 | b.Run("id=uuid", func(b *testing.B) { 351 | id := uuid.Must(uuid.NewV7()) 352 | 353 | b.ReportAllocs() 354 | b.ResetTimer() 355 | for i := 0; i < b.N; i++ { 356 | id.Bytes() 357 | } 358 | }) 359 | } 360 | 361 | func BenchmarkNewWithPrefix(b *testing.B) { 362 | for i := 0; i < b.N; i++ { 363 | _ = typeid.Must(typeid.WithPrefix("prefix")) 364 | } 365 | } 366 | 367 | func BenchmarkEncodeDecode(b *testing.B) { 368 | for i := 0; i < b.N; i++ { 369 | tid := typeid.Must(typeid.WithPrefix("prefix")) 370 | _ = typeid.Must(typeid.FromString(tid.String())) 371 | } 372 | } 373 | 374 | // Benchmark Base32 operations directly 375 | func BenchmarkBase32(b *testing.B) { 376 | b.Run("encode", func(b *testing.B) { 377 | uid := uuid.Must(uuid.NewV7()) 378 | var bytes [16]byte 379 | copy(bytes[:], uid.Bytes()) 380 | b.ReportAllocs() 381 | b.ResetTimer() 382 | for i := 0; i < b.N; i++ { 383 | _ = base32.Encode(bytes) 384 | } 385 | }) 386 | b.Run("decode", func(b *testing.B) { 387 | uid := uuid.Must(uuid.NewV7()) 388 | var bytes [16]byte 389 | copy(bytes[:], uid.Bytes()) 390 | encoded := base32.Encode(bytes) 391 | b.ReportAllocs() 392 | b.ResetTimer() 393 | for i := 0; i < b.N; i++ { 394 | _, _ = base32.Decode(encoded) 395 | } 396 | }) 397 | } 398 | 399 | // Benchmark memory usage with different batch sizes 400 | func BenchmarkMemoryUsage(b *testing.B) { 401 | benchSizes := []int{100, 1000, 10000} 402 | for _, size := range benchSizes { 403 | size := size // capture range variable 404 | b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { 405 | // Pre-allocate a slice to avoid measuring slice growth 406 | ids := make([]typeid.AnyID, size) 407 | b.Cleanup(func() { 408 | // Clear the slice to help GC 409 | for i := range ids { 410 | ids[i] = typeid.AnyID{} 411 | } 412 | ids = nil 413 | }) 414 | 415 | b.ReportAllocs() 416 | b.SetBytes(int64(size * 16)) // Each UUID is 16 bytes 417 | b.ResetTimer() 418 | 419 | for i := 0; i < b.N; i++ { 420 | for j := range ids { 421 | ids[j] = typeid.Must(typeid.WithPrefix("prefix")) 422 | } 423 | // Force memory pressure to ensure GC behavior is measured 424 | runtime.GC() 425 | } 426 | }) 427 | } 428 | } 429 | 430 | // Benchmark validation 431 | func BenchmarkValidation(b *testing.B) { 432 | validIDs := make([]string, 100) 433 | invalidIDs := make([]string, 100) 434 | 435 | for i := range validIDs { 436 | validIDs[i] = typeid.Must(typeid.WithPrefix("prefix")).String() 437 | if i < len(invalidIDs) { 438 | // Create definitely invalid IDs by: 439 | // 1. Using invalid prefix characters 440 | // 2. Wrong separator 441 | // 3. Invalid base32 characters 442 | switch i % 3 { 443 | case 0: 444 | // Invalid prefix (contains number) 445 | invalidIDs[i] = "prefix1_01h2xcejqtf2nbrexx3vqjhp41" 446 | case 1: 447 | // Wrong separator (using . instead of _) 448 | invalidIDs[i] = "prefix.01h2xcejqtf2nbrexx3vqjhp41" 449 | case 2: 450 | // Invalid base32 character in suffix (using 'u' which isn't in the alphabet) 451 | invalidIDs[i] = "prefix_u1h2xcejqtf2nbrexx3vqjhp41" 452 | } 453 | } 454 | } 455 | 456 | b.Run("valid", func(b *testing.B) { 457 | b.ReportAllocs() 458 | b.ResetTimer() 459 | 460 | for i := 0; i < b.N; i++ { 461 | // Test all valid IDs in each iteration 462 | for _, id := range validIDs { 463 | _, err := typeid.FromString(id) 464 | if err != nil { 465 | b.Fatalf("Expected valid ID to pass validation: %v", err) 466 | } 467 | } 468 | } 469 | }) 470 | 471 | b.Run("invalid", func(b *testing.B) { 472 | b.ReportAllocs() 473 | b.ResetTimer() 474 | 475 | for i := 0; i < b.N; i++ { 476 | // Test all invalid IDs in each iteration 477 | for _, id := range invalidIDs { 478 | _, err := typeid.FromString(id) 479 | if err == nil { 480 | b.Fatalf("Expected invalid ID to fail validation for ID: %s", id) 481 | } 482 | } 483 | } 484 | }) 485 | } 486 | 487 | // Benchmark parallel ID generation 488 | func BenchmarkParallelGeneration(b *testing.B) { 489 | benchCases := []struct { 490 | name string 491 | procs int 492 | batchSize int 493 | }{ 494 | {"procs=4_batch=100", 4, 100}, 495 | {"procs=8_batch=100", runtime.GOMAXPROCS(0) * 2, 100}, 496 | {"procs=4_batch=1000", 4, 1000}, 497 | {"procs=8_batch=1000", runtime.GOMAXPROCS(0) * 2, 1000}, 498 | } 499 | 500 | for _, bc := range benchCases { 501 | bc := bc // capture range variable 502 | b.Run(bc.name, func(b *testing.B) { 503 | // Pre-allocate a slice of prefixes for each processor 504 | prefixes := make([]string, bc.procs) 505 | for i := range prefixes { 506 | // Use valid prefixes with only [a-z_] 507 | prefixes[i] = fmt.Sprintf("prefix_%c", 'a'+i) 508 | } 509 | 510 | b.SetParallelism(bc.procs) 511 | b.ReportAllocs() 512 | b.ResetTimer() 513 | 514 | b.RunParallel(func(pb *testing.PB) { 515 | // Pre-allocate slice with capacity 516 | ids := make([]typeid.AnyID, 0, bc.batchSize) 517 | // Keep track of which prefix we're using 518 | prefixIdx := 0 519 | 520 | for pb.Next() { 521 | // Clear the slice but keep capacity 522 | ids = ids[:0] 523 | 524 | // Use all prefixes in each iteration 525 | for j := 0; j < bc.batchSize; j++ { 526 | // Cycle through prefixes 527 | prefix := prefixes[prefixIdx%len(prefixes)] 528 | prefixIdx++ 529 | ids = append(ids, typeid.Must(typeid.WithPrefix(prefix))) 530 | } 531 | } 532 | }) 533 | }) 534 | } 535 | } 536 | 537 | // Benchmark mixed operations to simulate real-world usage patterns 538 | func BenchmarkMixedOperations(b *testing.B) { 539 | // Pre-generate all operations and test data 540 | type op struct { 541 | kind string 542 | id typeid.AnyID // For operations that need an existing ID 543 | } 544 | 545 | // Create a slice of operations in the ratio we want to test 546 | numOps := 100 // Number of operations to pre-generate 547 | ops := make([]op, numOps) 548 | ids := make([]typeid.AnyID, numOps/2) // Pre-generate some IDs for operations that need them 549 | 550 | for i := range ids { 551 | ids[i] = typeid.Must(typeid.WithPrefix("prefix")) 552 | } 553 | 554 | // Initialize random number generator 555 | src := rand.NewSource(1234) 556 | rnd := rand.New(src) 557 | 558 | // Generate operations in desired ratios 559 | for i := range ops { 560 | r := rnd.Float32() 561 | switch { 562 | case r < 0.1: // 10% new IDs 563 | ops[i] = op{kind: "create"} 564 | case r < 0.4: // 30% toString 565 | ops[i] = op{kind: "toString", id: ids[rnd.Intn(len(ids))]} 566 | case r < 0.7: // 30% parse 567 | ops[i] = op{kind: "parse", id: ids[rnd.Intn(len(ids))]} 568 | default: // 30% validate 569 | ops[i] = op{kind: "validate", id: ids[rnd.Intn(len(ids))]} 570 | } 571 | } 572 | 573 | b.ReportAllocs() 574 | b.ResetTimer() 575 | 576 | // Ensure we go through all operations at least once 577 | for i := 0; i < b.N; i++ { 578 | // Do all operations once per iteration 579 | for _, op := range ops { 580 | switch op.kind { 581 | case "create": 582 | _ = typeid.Must(typeid.WithPrefix("prefix")) 583 | case "toString": 584 | _ = op.id.String() 585 | case "parse": 586 | _, _ = typeid.FromString(op.id.String()) 587 | case "validate": 588 | _, _ = typeid.FromString(op.id.String()) 589 | } 590 | } 591 | } 592 | } 593 | 594 | // TODO: define these in a shared file if we're gonna use in several tests. 595 | 596 | type TestPrefix struct{} 597 | 598 | func (TestPrefix) Prefix() string { return "prefix" } 599 | 600 | type TestID struct { 601 | typeid.TypeID[TestPrefix] 602 | } 603 | -------------------------------------------------------------------------------- /constructors.go: -------------------------------------------------------------------------------- 1 | package typeid 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/gofrs/uuid/v5" 9 | "go.jetify.com/typeid/base32" 10 | ) 11 | 12 | var ErrConstructor = errors.New("constructor error") 13 | 14 | // New returns a new TypeID of the given type with a random suffix. 15 | // 16 | // Use the generic argument to pass in your typeid Subtype: 17 | // 18 | // Example: 19 | // 20 | // type UserID struct { 21 | // typeid.TypeID[UserPrefix] 22 | // } 23 | // id, err := typeid.New[UserID]() 24 | func New[T Subtype, PT SubtypePtr[T]]() (T, error) { 25 | if isAnyID[T]() { 26 | var id T 27 | return id, errors.New("constructor error: use WithPrefix(), New() is for Subtypes") 28 | } 29 | 30 | prefix := defaultType[T]() 31 | return from[T, PT](prefix, "") 32 | } 33 | 34 | // WithPrefix returns a new TypeID with the given prefix and a random suffix. 35 | // If you want to create an id without a prefix, pass an empty string. 36 | func WithPrefix(prefix string) (AnyID, error) { 37 | return from[AnyID](prefix, "") 38 | } 39 | 40 | // From returns a new TypeID with the given prefix and suffix. 41 | // If suffix is the empty string, a random suffix will be generated. 42 | // If you want to create an id without a prefix, pass an empty string as the prefix. 43 | func From(prefix string, suffix string) (AnyID, error) { 44 | return from[AnyID](prefix, suffix) 45 | } 46 | 47 | // FromSuffix returns a new TypeID of the given suffix and type. The prefix 48 | // is inferred from the Subtype. 49 | // 50 | // Example: 51 | // 52 | // type UserID struct { 53 | // typeid.TypeID[UserPrefix] 54 | // } 55 | // id, err := typeid.FromSuffix[UserID]("00041061050r3gg28a1c60t3gf") 56 | func FromSuffix[T Subtype, PT SubtypePtr[T]](suffix string) (T, error) { 57 | if isAnyID[T]() { 58 | var id T 59 | return id, errors.New("constructor error: use From(prefix, suffix), FromSuffix is for Subtypes") 60 | } 61 | 62 | prefix := defaultType[T]() 63 | return parse[T, PT](prefix, suffix) 64 | } 65 | 66 | // FromString parses a TypeID from a string of the form _ 67 | func FromString(s string) (AnyID, error) { 68 | return Parse[AnyID](s) 69 | } 70 | 71 | // Parse parses a TypeID from a string of the form _ 72 | // and ensures the TypeID is of the right type. 73 | // 74 | // Example: 75 | // 76 | // type UserID struct { 77 | // typeid.TypeID[UserPrefix] 78 | // } 79 | // id, err := typeid.Parse[UserID]("user_00041061050r3gg28a1c60t3gf") 80 | func Parse[T Subtype, PT SubtypePtr[T]](s string) (T, error) { 81 | prefix, suffix, err := split(s) 82 | if err != nil { 83 | var id T 84 | return id, err 85 | } 86 | return parse[T, PT](prefix, suffix) 87 | } 88 | 89 | func split(id string) (string, string, error) { 90 | index := strings.LastIndex(id, "_") 91 | if index == -1 { 92 | return "", id, nil 93 | } 94 | 95 | prefix := id[:index] 96 | suffix := id[index+1:] 97 | if prefix == "" { 98 | return "", "", errors.New("prefix cannot be empty when there's a separator") 99 | } 100 | return prefix, suffix, nil 101 | } 102 | 103 | // FromUUID encodes the given UUID (in hex string form) as a TypeID 104 | func FromUUID[T Subtype, PT SubtypePtr[T]](uidStr string) (T, error) { 105 | if isAnyID[T]() { 106 | var id T 107 | return id, fmt.Errorf( 108 | "%w: use FromUUIDWithPrefix(), FromUUID() is for Subtypes", 109 | ErrConstructor, 110 | ) 111 | } 112 | return fromUUID[T, PT](defaultPrefix[T](), uidStr) 113 | } 114 | 115 | // FromUUIDBytes encodes the given UUID (in byte form) as a TypeID 116 | func FromUUIDBytes[T Subtype, PT SubtypePtr[T]](bytes []byte) (T, error) { 117 | if isAnyID[T]() { 118 | var id T 119 | return id, fmt.Errorf( 120 | "%w: use FromUUIDBytesWithPrefix(), FromUUIDBytes() is for Subtypes", 121 | ErrConstructor, 122 | ) 123 | } 124 | uidStr := uuid.FromBytesOrNil(bytes).String() 125 | return FromUUID[T, PT](uidStr) 126 | } 127 | 128 | // FromUUIDWithPrefix encodes the given UUID (in hex string form) as a TypeID 129 | // with the given prefix. 130 | func FromUUIDWithPrefix(prefix string, uidStr string) (AnyID, error) { 131 | return fromUUID[AnyID](prefix, uidStr) 132 | } 133 | 134 | // FromUUID encodes the given UUID (in byte form) as a TypeID with the given 135 | // prefix. 136 | func FromUUIDBytesWithPrefix(prefix string, bytes []byte) (AnyID, error) { 137 | uidStr := uuid.FromBytesOrNil(bytes).String() 138 | return FromUUIDWithPrefix(prefix, uidStr) 139 | } 140 | 141 | func fromUUID[T Subtype, PT SubtypePtr[T]](prefix, uidStr string) (T, error) { 142 | uid, err := uuid.FromString(uidStr) 143 | var nilID T 144 | 145 | if err != nil { 146 | return nilID, err 147 | } 148 | suffix := base32.Encode(uid) 149 | return parse[T, PT](prefix, suffix) 150 | } 151 | 152 | func parse[T Subtype, PT SubtypePtr[T]](prefix string, suffix string) (T, error) { 153 | if suffix == "" { 154 | var id T 155 | return id, errors.New("suffix can't be the empty string") 156 | } 157 | return from[T, PT](prefix, suffix) 158 | } 159 | 160 | func from[T Subtype, PT SubtypePtr[T]](prefix string, suffix string) (T, error) { 161 | var tid T 162 | if err := validatePrefix[T](prefix); err != nil { 163 | return tid, err 164 | } 165 | 166 | if suffix == "" { 167 | uid, err := uuid.NewV7() 168 | if err != nil { 169 | return tid, err 170 | } 171 | suffix = base32.Encode(uid) 172 | } 173 | 174 | if err := validateSuffix(suffix); err != nil { 175 | return tid, err 176 | } 177 | 178 | PT(&tid).init(prefix, suffix) 179 | return tid, nil 180 | } 181 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // TypeIDs are a modern, **type-safe**, globally unique identifier based on the upcoming 2 | // UUIDv7 standard. They provide a ton of nice properties that make them a great choice 3 | // as the primary identifiers for your data in a database, APIs, and distributed systems. 4 | // Read more about TypeIDs in their [spec](https://github.com/jetify-com/typeid). 5 | 6 | // This particular implementation provides a go library for generating and parsing TypeIDs 7 | package typeid 8 | -------------------------------------------------------------------------------- /encoding.go: -------------------------------------------------------------------------------- 1 | package typeid 2 | 3 | import ( 4 | "encoding" 5 | ) 6 | 7 | // TODO: Define a standardized binary encoding for typeids in the spec 8 | // and use that to implement encoding.BinaryMarshaler and encoding.BinaryUnmarshaler 9 | 10 | var ( 11 | _ encoding.TextMarshaler = (*TypeID[AnyPrefix])(nil) 12 | _ encoding.TextUnmarshaler = (*TypeID[AnyPrefix])(nil) 13 | ) 14 | 15 | // UnmarshalText implements the encoding.TextUnmarshaler interface. 16 | // It parses a TypeID from a string using the same logic as FromString() 17 | func (tid *TypeID[P]) UnmarshalText(text []byte) error { 18 | parsed, err := Parse[TypeID[P]](string(text)) 19 | if err != nil { 20 | return err 21 | } 22 | *tid = parsed 23 | return nil 24 | } 25 | 26 | // MarshalText implements the encoding.TextMarshaler interface. 27 | // It encodes a TypeID as a string using the same logic as String() 28 | func (tid TypeID[P]) MarshalText() (text []byte, err error) { 29 | encoded := tid.String() 30 | return []byte(encoded), nil 31 | } 32 | -------------------------------------------------------------------------------- /encoding_test.go: -------------------------------------------------------------------------------- 1 | package typeid_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "go.jetify.com/typeid" 10 | ) 11 | 12 | func TestJSON(t *testing.T) { 13 | str := "prefix_00041061050r3gg28a1c60t3gf" 14 | tid := typeid.Must(typeid.FromString(str)) 15 | 16 | encoded, err := json.Marshal(tid) 17 | assert.NoError(t, err) 18 | assert.Equal(t, `"`+str+`"`, string(encoded)) 19 | 20 | var decoded typeid.AnyID 21 | err = json.Unmarshal(encoded, &decoded) 22 | assert.NoError(t, err) 23 | 24 | assert.Equal(t, tid, decoded) 25 | assert.Equal(t, str, decoded.String()) 26 | } 27 | 28 | func TestJSON_Subtype(t *testing.T) { 29 | str := "user_00041061050r3gg28a1c60t3gf" 30 | tid := typeid.Must(typeid.Parse[UserID](str)) 31 | 32 | encoded, err := json.Marshal(tid) 33 | assert.NoError(t, err) 34 | assert.Equal(t, `"`+str+`"`, string(encoded)) 35 | 36 | var decoded UserID 37 | err = json.Unmarshal(encoded, &decoded) 38 | assert.NoError(t, err) 39 | 40 | assert.Equal(t, tid, decoded) 41 | assert.Equal(t, str, decoded.String()) 42 | 43 | var wrongType AccountID 44 | err = json.Unmarshal(encoded, &wrongType) 45 | assert.Error(t, err) 46 | } 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.jetify.com/typeid 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/gofrs/uuid/v5 v5.3.2 7 | github.com/stretchr/testify v1.10.0 8 | gopkg.in/yaml.v2 v2.4.0 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/kr/pretty v0.3.1 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | github.com/rogpeppe/go-internal v1.14.1 // indirect 16 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 17 | gopkg.in/yaml.v3 v3.0.1 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0= 5 | github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= 6 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 7 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 8 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 9 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 10 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 11 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 12 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 13 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 17 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 18 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 19 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 20 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 22 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 23 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 24 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 25 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 28 | -------------------------------------------------------------------------------- /prefix.go: -------------------------------------------------------------------------------- 1 | package typeid 2 | 3 | // PrefixType is the interface that defines the type if a type id. 4 | // Implement your own version of this interface if you want to define a custom 5 | // type: 6 | // type UserPrefix struct {} 7 | // func (UserPrefix) Prefix() string { return "user" } 8 | type PrefixType interface { 9 | Prefix() string 10 | } 11 | 12 | // Any is a special prefix that can be used to represent TypeIDs that allow for 13 | // any valid prefix. 14 | type AnyPrefix struct{} 15 | 16 | func (a AnyPrefix) Prefix() string { 17 | return "*" // Any is treated specially, so in practice this string will never be used. 18 | } 19 | 20 | // AnyID represents TypeIDs that accept any valid prefix. 21 | type AnyID struct { 22 | TypeID[AnyPrefix] 23 | } 24 | 25 | func isAnyPrefix[P PrefixType]() bool { 26 | var prefixType P 27 | switch any(prefixType).(type) { 28 | case AnyPrefix: 29 | return true 30 | default: 31 | return false 32 | } 33 | } 34 | 35 | func defaultPrefix[P PrefixType]() string { 36 | var prefixType P 37 | return prefixType.Prefix() 38 | } 39 | -------------------------------------------------------------------------------- /sql.go: -------------------------------------------------------------------------------- 1 | package typeid 2 | 3 | import ( 4 | "database/sql/driver" 5 | "fmt" 6 | ) 7 | 8 | // Scan implements the sql.Scanner interface so the TypeIDs can be read from 9 | // databases transparently. Currently database types that map to string are 10 | // supported. 11 | func (tid *TypeID[P]) Scan(src any) error { 12 | switch obj := src.(type) { 13 | case nil: 14 | return nil 15 | case string: 16 | if src == "" { 17 | return nil 18 | } 19 | return tid.UnmarshalText([]byte(obj)) 20 | // TODO: add support for []byte 21 | // we don't just want to store the full string as a byte array. Instead 22 | // we should encode using the UUID bytes. We could add support for 23 | // Binary Marshalling and Unmarshalling at the same time. 24 | default: 25 | return fmt.Errorf("unsupported scan type %T", obj) 26 | } 27 | } 28 | 29 | // Value implements the sql.Valuer interface so that TypeIDs can be written 30 | // to databases transparently. Currently, TypeIDs map to strings. 31 | func (tid TypeID[P]) Value() (driver.Value, error) { 32 | return tid.String(), nil 33 | } 34 | -------------------------------------------------------------------------------- /sql_test.go: -------------------------------------------------------------------------------- 1 | package typeid_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "go.jetify.com/typeid" 8 | ) 9 | 10 | func TestScan(t *testing.T) { 11 | testdata := []struct { 12 | name string 13 | input any 14 | expected typeid.AnyID 15 | }{ 16 | {"valid", "prefix_00041061050r3gg28a1c60t3gf", typeid.Must(typeid.FromString("prefix_00041061050r3gg28a1c60t3gf"))}, 17 | {"nil", nil, typeid.AnyID{}}, 18 | {"empty string", "", typeid.AnyID{}}, 19 | } 20 | 21 | for _, td := range testdata { 22 | t.Run(td.name, func(t *testing.T) { 23 | var scanned typeid.AnyID 24 | err := scanned.Scan(td.input) 25 | assert.NoError(t, err) 26 | 27 | assert.Equal(t, td.expected, scanned) 28 | assert.Equal(t, td.expected.String(), scanned.String()) 29 | }) 30 | } 31 | } 32 | 33 | func TestValuer(t *testing.T) { 34 | expected := "prefix_00041061050r3gg28a1c60t3gf" 35 | tid := typeid.Must(typeid.FromString("prefix_00041061050r3gg28a1c60t3gf")) 36 | actual, err := tid.Value() 37 | assert.NoError(t, err) 38 | assert.Equal(t, expected, actual) 39 | } 40 | -------------------------------------------------------------------------------- /subtype.go: -------------------------------------------------------------------------------- 1 | package typeid 2 | 3 | // Subtype is an interface used to create a more specific subtype of TypeID 4 | // For example, if you want to create an `OrgID` type that only accepts 5 | // an `org_` prefix. 6 | type Subtype interface { 7 | Prefix() string 8 | Suffix() string 9 | String() string 10 | UUIDBytes() []byte 11 | UUID() string 12 | 13 | isTypeID() bool 14 | } 15 | 16 | var _ Subtype = (*TypeID[AnyPrefix])(nil) 17 | 18 | type SubtypePtr[T any] interface { 19 | *T 20 | init(prefix string, suffix string) 21 | } 22 | 23 | func (tid *TypeID[P]) init(prefix string, suffix string) { 24 | // In general TypeID is an immutable value-type, and pretty much every 25 | // "mutation" should return a copy with the modifications instead of modifying 26 | // the original. We make an exception for this *private* method, because 27 | // sometimes we need to modify the fields in the process of initializing 28 | // a new subtype. 29 | 30 | // Only store the prefix if dealing with a subtype: 31 | if isAnyPrefix[P]() { 32 | tid.prefix = prefix 33 | } 34 | 35 | // If we're dealing with the "zero" suffix, we don't need to store it. 36 | if suffix != zeroSuffix { 37 | tid.suffix = suffix 38 | } 39 | } 40 | 41 | func (tid TypeID[P]) isTypeID() bool { 42 | return true 43 | } 44 | 45 | func isAnyID[T Subtype]() bool { 46 | var id T 47 | switch any(id).(type) { 48 | case TypeID[AnyPrefix]: 49 | return true 50 | case AnyID: 51 | return true 52 | default: 53 | return false 54 | } 55 | } 56 | 57 | func defaultType[T Subtype]() string { 58 | var id T 59 | return id.Prefix() 60 | } 61 | -------------------------------------------------------------------------------- /subtype_example_test.go: -------------------------------------------------------------------------------- 1 | package typeid_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.jetify.com/typeid" 7 | ) 8 | 9 | // To create a new id type, first implement a custom PrefixType and ensure the 10 | // Prefix() method returns the correct prefix 11 | type UserPrefix struct{} 12 | 13 | func (UserPrefix) Prefix() string { 14 | return "user" 15 | } 16 | 17 | // And then define your custom id type by embedding TypeID: 18 | type UserID struct { 19 | typeid.TypeID[UserPrefix] 20 | } 21 | 22 | // Now do the same for AccountIDs 23 | type AccountPrefix struct{} 24 | 25 | func (AccountPrefix) Prefix() string { 26 | return "account" 27 | } 28 | 29 | type AccountID struct { 30 | typeid.TypeID[AccountPrefix] 31 | } 32 | 33 | func Example() { 34 | // To create new IDs call typeid.New and pass your custom id type as the 35 | // generic argument: 36 | userID, _ := typeid.New[UserID]() 37 | accountID, _ := typeid.New[AccountID]() 38 | 39 | // Other than that, your custom types should have the same methods as a 40 | // regular TypeID. 41 | // For example, we can check that each ID has the correct type prefix: 42 | fmt.Printf("User ID prefix: %s\n", userID.Prefix()) 43 | fmt.Printf("Account ID prefix: %s\n", accountID.Prefix()) 44 | 45 | // Despite both of them being TypeIDs, you now get compile-time safety because 46 | // the compiler considers their go types to be different: 47 | // (typeid_test.UserID vs typeid_test.AccountID vs typeid.TypeID) 48 | fmt.Printf("%T != %T\n", userID, accountID) 49 | // Output: 50 | // User ID prefix: user 51 | // Account ID prefix: account 52 | // typeid_test.UserID != typeid_test.AccountID 53 | } 54 | -------------------------------------------------------------------------------- /subtype_test.go: -------------------------------------------------------------------------------- 1 | package typeid_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gofrs/uuid/v5" 8 | "github.com/stretchr/testify/assert" 9 | "go.jetify.com/typeid" 10 | ) 11 | 12 | func ExampleNew() { 13 | tid := typeid.Must(typeid.New[AccountID]()) 14 | fmt.Println("Prefix:", tid.Prefix()) 15 | // Output: 16 | // Prefix: account 17 | } 18 | 19 | func ExampleFromSuffix() { 20 | tid := typeid.Must(typeid.FromSuffix[UserID]("00041061050r3gg28a1c60t3gf")) 21 | fmt.Printf("Prefix: %s\nSuffix: %s\n", tid.Prefix(), tid.Suffix()) 22 | // Output: 23 | // Prefix: user 24 | // Suffix: 00041061050r3gg28a1c60t3gf 25 | } 26 | 27 | func TestSubtypeConstructors(t *testing.T) { 28 | // These constructors should work for a subtype: 29 | _, err := typeid.New[AccountID]() 30 | assert.NoError(t, err) 31 | _, err = typeid.FromSuffix[AccountID]("00041061050r3gg28a1c60t3gf") 32 | assert.NoError(t, err) 33 | 34 | // But error on TypeID[typeid.Any]: 35 | _, err = typeid.New[typeid.AnyID]() 36 | assert.Error(t, err) 37 | _, err = typeid.FromSuffix[typeid.AnyID]("00041061050r3gg28a1c60t3gf") 38 | assert.Error(t, err) 39 | } 40 | 41 | func TestSubtypeNil(t *testing.T) { 42 | var emptyUser UserID 43 | nilUser, err := typeid.Parse[UserID]("user_00000000000000000000000000") 44 | assert.NoError(t, err) 45 | assert.Equal(t, nilUser, emptyUser) 46 | assert.Equal(t, nilUser.String(), emptyUser.String()) 47 | assert.Equal(t, nilUser.Prefix(), emptyUser.Prefix()) 48 | assert.Equal(t, nilUser.UUID(), emptyUser.UUID()) 49 | assert.Equal(t, nilUser.UUIDBytes(), emptyUser.UUIDBytes()) 50 | assert.Equal(t, "user_00000000000000000000000000", nilUser.String()) 51 | assert.Equal(t, "user", nilUser.Prefix()) 52 | 53 | parsed, err := typeid.FromString("user_00000000000000000000000000") 54 | assert.NoError(t, err) 55 | assert.Equal(t, "user_00000000000000000000000000", parsed.String()) 56 | assert.Equal(t, "user", parsed.Prefix()) 57 | assert.Equal(t, "00000000000000000000000000", parsed.Suffix()) 58 | } 59 | 60 | func TestParse(t *testing.T) { 61 | // Generate a bunch of random UserIDs. We should be able to parse them 62 | // using the correct type, but not an incorrect one. 63 | for i := 0; i < 1000; i++ { 64 | tid := typeid.Must(typeid.New[UserID]()) 65 | // They parse as UserID 66 | parsed, err := typeid.Parse[UserID](tid.String()) 67 | if err != nil { 68 | t.Error(err) 69 | } 70 | if tid != parsed { 71 | t.Errorf("Expected %s, got %s", tid, parsed) 72 | } 73 | 74 | // They also parse as a generic TypeID 75 | _, err = typeid.FromString(tid.String()) 76 | if err != nil { 77 | t.Error(err) 78 | } 79 | 80 | // But not as an AccountID 81 | _, err = typeid.Parse[AccountID](tid.String()) 82 | assert.Error(t, err) 83 | } 84 | } 85 | 86 | func TestFromUUID(t *testing.T) { 87 | uid, err := uuid.NewV7() 88 | assert.NoError(t, err) 89 | id, err := typeid.FromUUID[UserID](uid.String()) 90 | assert.NoError(t, err) 91 | assert.Equal(t, uid.String(), id.UUID()) 92 | } 93 | 94 | func TestFromUUIDBytes(t *testing.T) { 95 | uid, err := uuid.NewV7() 96 | assert.NoError(t, err) 97 | id, err := typeid.FromUUIDBytes[UserID](uid.Bytes()) 98 | assert.NoError(t, err) 99 | assert.Equal(t, uid.Bytes(), id.UUIDBytes()) 100 | } 101 | -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | These test data files are a copy of the ones provided by the official 2 | [TypeID specification](https://github.com/jetify-com/typeid) 3 | -------------------------------------------------------------------------------- /testdata/invalid.yml: -------------------------------------------------------------------------------- 1 | # This file contains test data that should be treated as *invalid* TypeIDs by 2 | # conforming implementations. 3 | # 4 | # Each example contains an invalid TypeID string. Implementations are expected 5 | # to throw an error when attempting to parse/validate these strings. 6 | # 7 | # Last updated: 2024-05-18 (for version 0.3.0 of the spec) 8 | 9 | - name: prefix-uppercase 10 | typeid: "PREFIX_00000000000000000000000000" 11 | description: "The prefix should be lowercase with no uppercase letters" 12 | 13 | - name: prefix-numeric 14 | typeid: "12345_00000000000000000000000000" 15 | description: "The prefix can't have numbers, it needs to be alphabetic" 16 | 17 | - name: prefix-period 18 | typeid: "pre.fix_00000000000000000000000000" 19 | description: "The prefix can't have symbols, it needs to be alphabetic" 20 | 21 | # Test removed in v0.3.0 – we now allow underscores in the prefix 22 | # - name: prefix-underscore 23 | # typeid: "pre_fix_00000000000000000000000000" 24 | # description: "The prefix can't have symbols, it needs to be alphabetic" 25 | 26 | - name: prefix-non-ascii 27 | typeid: "préfix_00000000000000000000000000" 28 | description: "The prefix can only have ascii letters" 29 | 30 | - name: prefix-spaces 31 | typeid: " prefix_00000000000000000000000000" 32 | description: "The prefix can't have any spaces" 33 | 34 | - name: prefix-64-chars 35 | # 123456789 123456789 123456789 123456789 123456789 123456789 1234 36 | typeid: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl_00000000000000000000000000" 37 | description: "The prefix can't be 64 characters, it needs to be 63 characters or less" 38 | 39 | - name: separator-empty-prefix 40 | typeid: "_00000000000000000000000000" 41 | description: "If the prefix is empty, the separator should not be there" 42 | 43 | - name: separator-empty 44 | typeid: "_" 45 | description: "A separator by itself should not be treated as the empty string" 46 | 47 | - name: suffix-short 48 | typeid: "prefix_1234567890123456789012345" 49 | description: "The suffix can't be 25 characters, it needs to be exactly 26 characters" 50 | 51 | - name: suffix-long 52 | typeid: "prefix_123456789012345678901234567" 53 | description: "The suffix can't be 27 characters, it needs to be exactly 26 characters" 54 | 55 | - name: suffix-spaces 56 | # This example has the right length, so that the failure is caused by the space 57 | # and not the suffix length 58 | typeid: "prefix_1234567890123456789012345 " 59 | description: "The suffix can't have any spaces" 60 | 61 | - name: suffix-uppercase 62 | # This example is picked because it would be valid in lowercase 63 | typeid: "prefix_0123456789ABCDEFGHJKMNPQRS" 64 | description: "The suffix should be lowercase with no uppercase letters" 65 | 66 | - name: suffix-hyphens 67 | # This example has the right length, so that the failure is caused by the hyphens 68 | # and not the suffix length 69 | typeid: "prefix_123456789-123456789-123456" 70 | description: "The suffix can't have any hyphens" 71 | 72 | - name: suffix-wrong-alphabet 73 | typeid: "prefix_ooooooiiiiiiuuuuuuulllllll" 74 | description: "The suffix should only have letters from the spec's alphabet" 75 | 76 | - name: suffix-ambiguous-crockford 77 | # This example would be valid if we were using the crockford disambiguation rules 78 | typeid: "prefix_i23456789ol23456789oi23456" 79 | description: "The suffix should not have any ambiguous characters from the crockford encoding" 80 | 81 | - name: suffix-hyphens-crockford 82 | # This example would be valid if we were using the crockford hyphenation rules 83 | typeid: "prefix_123456789-0123456789-0123456" 84 | description: "The suffix can't ignore hyphens as in the crockford encoding" 85 | 86 | - name: suffix-overflow 87 | # This is the first suffix that overflows into 129 bits 88 | typeid: "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz" 89 | description: "The suffix should encode at most 128-bits" 90 | 91 | # Tests below were added in v0.3.0 when we started allowing '_' within the 92 | # type prefix. 93 | - name: prefix-underscore-start 94 | typeid: "_prefix_00000000000000000000000000" 95 | description: "The prefix can't start with an underscore" 96 | 97 | - name: prefix-underscore-end 98 | typeid: "prefix__00000000000000000000000000" 99 | description: "The prefix can't end with an underscore" 100 | 101 | - name: empty 102 | typeid: "" 103 | description: "The empty string is not a valid typeid" 104 | 105 | - name: prefix-empty 106 | typeid: "prefix_" 107 | description: "The suffix can't be the empty string" 108 | -------------------------------------------------------------------------------- /testdata/valid.yml: -------------------------------------------------------------------------------- 1 | # This file contains test data that should parse as valid TypeIDs by conforming 2 | # implementations. 3 | # 4 | # Each example contains: 5 | # - The TypeID in its canonical string representation. 6 | # - The prefix 7 | # - The decoded UUID as a hex string 8 | # 9 | # Implementations should verify that they can encode/decode the data 10 | # in both directions: 11 | # 1. If the TypeID is decoded, it should result in the given prefix and UUID. 12 | # 2. If the UUID is encoded as a TypeID with the given prefix, it should 13 | # result in the given TypeID. 14 | # 15 | # In addition to using these examples, it's recommended that implementations 16 | # generate a thousands of random ids during testing, and verify that after 17 | # decoding and re-encoding the id, the result is the same as the original. 18 | # 19 | # In other words, the following property should always hold: 20 | # random_typeid == encode(decode(random_typeid)) 21 | # 22 | # Finally, while implementations should be able to decode the values below, 23 | # note that not all of them are UUIDv7s. When *generating* new random typeids, 24 | # implementations should always use UUIDv7s. 25 | # 26 | # Last updated: 2024-04-10 (for version 0.3.0 of the spec) 27 | 28 | - name: nil 29 | typeid: "00000000000000000000000000" 30 | prefix: "" 31 | uuid: "00000000-0000-0000-0000-000000000000" 32 | 33 | - name: one 34 | typeid: "00000000000000000000000001" 35 | prefix: "" 36 | uuid: "00000000-0000-0000-0000-000000000001" 37 | 38 | - name: ten 39 | typeid: "0000000000000000000000000a" 40 | prefix: "" 41 | uuid: "00000000-0000-0000-0000-00000000000a" 42 | 43 | - name: sixteen 44 | typeid: "0000000000000000000000000g" 45 | prefix: "" 46 | uuid: "00000000-0000-0000-0000-000000000010" 47 | 48 | - name: thirty-two 49 | typeid: "00000000000000000000000010" 50 | prefix: "" 51 | uuid: "00000000-0000-0000-0000-000000000020" 52 | 53 | - name: max-valid 54 | typeid: "7zzzzzzzzzzzzzzzzzzzzzzzzz" 55 | prefix: "" 56 | uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff" 57 | 58 | - name: valid-alphabet 59 | typeid: "prefix_0123456789abcdefghjkmnpqrs" 60 | prefix: "prefix" 61 | uuid: "0110c853-1d09-52d8-d73e-1194e95b5f19" 62 | 63 | - name: valid-uuidv7 64 | typeid: "prefix_01h455vb4pex5vsknk084sn02q" 65 | prefix: "prefix" 66 | uuid: "01890a5d-ac96-774b-bcce-b302099a8057" 67 | 68 | # Tests below were added in v0.3.0 when we started allowing '_' within the 69 | # type prefix. 70 | - name: prefix-underscore 71 | typeid: "pre_fix_00000000000000000000000000" 72 | prefix: "pre_fix" 73 | uuid: "00000000-0000-0000-0000-000000000000" 74 | -------------------------------------------------------------------------------- /typeid.go: -------------------------------------------------------------------------------- 1 | package typeid 2 | 3 | import ( 4 | "github.com/gofrs/uuid/v5" 5 | "go.jetify.com/typeid/base32" 6 | ) 7 | 8 | // TypeID is a unique identifier with a given type as defined by the TypeID spec 9 | type TypeID[P PrefixType] struct { 10 | prefix string 11 | suffix string 12 | } 13 | 14 | // Prefix returns the type prefix of the TypeID 15 | func (tid TypeID[P]) Prefix() string { 16 | if isAnyPrefix[P]() { 17 | return tid.prefix 18 | } 19 | return defaultPrefix[P]() 20 | } 21 | 22 | const zeroSuffix = "00000000000000000000000000" 23 | 24 | // Suffix returns the suffix of the TypeID in it's canonical base32 representation. 25 | func (tid TypeID[P]) Suffix() string { 26 | // We want to treat the "empty" TypeID as equivalent to the 'zero' typeid 27 | if tid.suffix == "" { 28 | return zeroSuffix 29 | } 30 | return tid.suffix 31 | } 32 | 33 | // String returns the TypeID in it's canonical string representation of the form: 34 | // _ where is the canonical base32 representation of the UUID 35 | func (tid TypeID[P]) String() string { 36 | if tid.Prefix() == "" { 37 | return tid.Suffix() 38 | } 39 | return tid.Prefix() + "_" + tid.Suffix() 40 | } 41 | 42 | // UUIDBytes decodes the TypeID's suffix as a UUID and returns it's bytes 43 | func (tid TypeID[P]) UUIDBytes() []byte { 44 | b, err := base32.Decode(tid.Suffix()) 45 | // Decode only fails if the suffix cannot be decoded for one of two reasons: 46 | // 1. The suffix is not 26 characters long 47 | // 2. The suffix contains characters that are not in the base32 alphabet 48 | // We guarantee that the suffix is valid in the TypeID constructors, so this panic 49 | // should never be reached. 50 | if err != nil { 51 | panic(err) 52 | } 53 | return b 54 | } 55 | 56 | // UUID decodes the TypeID's suffix as a UUID and returns it as a hex string 57 | func (tid TypeID[P]) UUID() string { 58 | return uuid.FromBytesOrNil(tid.UUIDBytes()).String() 59 | } 60 | 61 | // IsZero returns true if the suffix of the TypeID is the zero suffix: 62 | // "00000000000000000000000000" 63 | // 64 | // Note that IsZero() returns true regardless of the prefix value. All 65 | // of these ids would return `IsZero == true`: 66 | // + "prefix_00000000000000000000000000" 67 | // + "test_00000000000000000000000000" 68 | // + "00000000000000000000000000" 69 | func (tid TypeID[P]) IsZero() bool { 70 | return tid.suffix == "" || tid.suffix == zeroSuffix 71 | } 72 | 73 | // Must returns a TypeID if the error is nil, otherwise panics. 74 | // Often used with New() to create a TypeID in a single line as follows: 75 | // tid := Must(New("prefix")) 76 | func Must[T any](tid T, err error) T { 77 | if err != nil { 78 | panic(err) 79 | } 80 | return tid 81 | } 82 | -------------------------------------------------------------------------------- /typeid_test.go: -------------------------------------------------------------------------------- 1 | package typeid_test 2 | 3 | import ( 4 | _ "embed" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "go.jetify.com/typeid" 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | func ExampleWithPrefix() { 15 | tid := typeid.Must(typeid.WithPrefix("prefix")) 16 | fmt.Printf("New typeid: %s\n", tid) 17 | } 18 | 19 | func ExampleWithPrefix_emptyPrefix() { 20 | tid := typeid.Must(typeid.WithPrefix("")) 21 | fmt.Printf("New typeid without prefix: %s\n", tid) 22 | } 23 | 24 | func ExampleFromString() { 25 | tid := typeid.Must(typeid.FromString("prefix_00041061050r3gg28a1c60t3gf")) 26 | fmt.Printf("Prefix: %s\nSuffix: %s\n", tid.Prefix(), tid.Suffix()) 27 | // Output: 28 | // Prefix: prefix 29 | // Suffix: 00041061050r3gg28a1c60t3gf 30 | } 31 | 32 | func TestNilIsEmpty(t *testing.T) { 33 | var emptyID typeid.AnyID 34 | nilID, err := typeid.FromString("00000000000000000000000000") 35 | assert.NoError(t, err) 36 | assert.Equal(t, nilID, emptyID) 37 | assert.Equal(t, nilID.String(), emptyID.String()) 38 | assert.Equal(t, nilID.UUID(), emptyID.UUID()) 39 | assert.Equal(t, nilID.UUIDBytes(), emptyID.UUIDBytes()) 40 | } 41 | 42 | func TestIsZero(t *testing.T) { 43 | testdata := []struct { 44 | input string 45 | output bool 46 | }{ 47 | // IsZero == true values 48 | {"00000000000000000000000000", true}, 49 | {"prefix_00000000000000000000000000", true}, 50 | {"other_00000000000000000000000000", true}, 51 | // IsZero == false values 52 | {"00000000000000000000000001", false}, 53 | {"prefix_00000000000000000000000001", false}, 54 | {"other_00000000000000000000000001", false}, 55 | } 56 | 57 | for _, td := range testdata { 58 | t.Run(td.input, func(t *testing.T) { 59 | tid, err := typeid.FromString(td.input) 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | if tid.IsZero() != td.output { 64 | t.Errorf("TypeId.IsZero should be %v for id %s", td.output, td.input) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func TestInvalidPrefix(t *testing.T) { 71 | testdata := []struct { 72 | name string 73 | input string 74 | }{ 75 | {"caps", "PREFIX"}, // Would be valid in lowercase 76 | {"numeric", "12323"}, 77 | {"symbols", "pre.fix"}, 78 | {"spaces", " "}, 79 | {"long", "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"}, 80 | } 81 | 82 | for _, td := range testdata { 83 | t.Run(td.name, func(t *testing.T) { 84 | _, err := typeid.WithPrefix(td.input) 85 | if err == nil { 86 | t.Errorf("Expected error for invalid prefix: %s", td.input) 87 | } 88 | }) 89 | } 90 | } 91 | 92 | func TestInvalidSuffix(t *testing.T) { 93 | testdata := []struct { 94 | name string 95 | input string 96 | }{ 97 | {"spaces", " "}, 98 | {"short", "01234"}, 99 | {"long", "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"}, 100 | {"caps", "00041061050R3GG28A1C60T3GF"}, // Would be valid in lowercase 101 | {"hyphens", "00041061050-3gg28a1-60t3gf"}, 102 | {"crockford_ambiguous", "ooo41o61o5or3gg28a1c6ot3gi"}, // Would be valid if we followed Crocksford's substitution rules 103 | {"symbols", "00041061050.3gg28a1_60t3gf"}, 104 | {"wrong_alphabet", "ooooooiiiiiiuuuuuuulllllll"}, 105 | } 106 | 107 | for _, td := range testdata { 108 | t.Run(td.name, func(t *testing.T) { 109 | _, err := typeid.From("prefix", td.input) 110 | if err == nil { 111 | t.Errorf("Expected error for invalid suffix: %s", td.input) 112 | } 113 | }) 114 | } 115 | } 116 | 117 | //go:embed testdata/invalid.yml 118 | var invalidYML []byte 119 | 120 | type InvalidExample struct { 121 | Name string `yaml:"name"` 122 | Tid string `yaml:"typeid"` 123 | } 124 | 125 | func TestInvalidTestdata(t *testing.T) { 126 | assert.Greater(t, len(invalidYML), 0) 127 | var testdata []InvalidExample 128 | err := yaml.Unmarshal(invalidYML, &testdata) 129 | if err != nil { 130 | t.Errorf("Failed to unmarshal testdata: %s", err) 131 | } 132 | assert.Greater(t, len(testdata), 0) 133 | 134 | for _, td := range testdata { 135 | t.Run(td.Name, func(t *testing.T) { 136 | _, err := typeid.FromString(td.Tid) 137 | if err == nil { 138 | t.Errorf("Expected error for invalid typeid: %s", td.Tid) 139 | } 140 | }) 141 | } 142 | } 143 | 144 | func TestEncodeDecode(t *testing.T) { 145 | // Generate a bunch of random typeids, encode and decode from a string 146 | // and make sure the result is the same as the original. 147 | for i := 0; i < 1000; i++ { 148 | tid := typeid.Must(typeid.WithPrefix("prefix")) 149 | decoded, err := typeid.FromString(tid.String()) 150 | if err != nil { 151 | t.Error(err) 152 | } 153 | if tid != decoded { 154 | t.Errorf("Expected %s, got %s", tid, decoded) 155 | } 156 | } 157 | 158 | // Repeat with the empty prefix: 159 | for i := 0; i < 1000; i++ { 160 | tid := typeid.Must(typeid.WithPrefix("")) 161 | decoded, err := typeid.FromString(tid.String()) 162 | if err != nil { 163 | t.Error(err) 164 | } 165 | if tid != decoded { 166 | t.Errorf("Expected %s, got %s", tid, decoded) 167 | } 168 | } 169 | } 170 | 171 | func TestSpecialValues(t *testing.T) { 172 | testdata := []struct { 173 | name string 174 | tid string 175 | uuid string 176 | }{ 177 | {"nil", "00000000000000000000000000", "00000000-0000-0000-0000-000000000000"}, 178 | {"one", "00000000000000000000000001", "00000000-0000-0000-0000-000000000001"}, 179 | {"ten", "0000000000000000000000000a", "00000000-0000-0000-0000-00000000000a"}, 180 | {"sixteen", "0000000000000000000000000g", "00000000-0000-0000-0000-000000000010"}, 181 | {"thirty-two", "00000000000000000000000010", "00000000-0000-0000-0000-000000000020"}, 182 | } 183 | for _, data := range testdata { 184 | t.Run(data.name, func(t *testing.T) { 185 | // Values should be equal if we start by parsing the typeid 186 | tid := typeid.Must(typeid.FromString(data.tid)) 187 | if data.uuid != tid.UUID() { 188 | t.Errorf("Expected %s, got %s", data.uuid, tid.UUID()) 189 | } 190 | 191 | // Values should be equal if we start by parsing the uuid 192 | tid = typeid.Must(typeid.FromUUIDWithPrefix("", data.uuid)) 193 | if data.tid != tid.String() { 194 | t.Errorf("Expected %s, got %s", data.tid, tid.String()) 195 | } 196 | }) 197 | } 198 | } 199 | 200 | //go:embed testdata/valid.yml 201 | var validYML []byte 202 | 203 | type ValidExample struct { 204 | Name string `yaml:"name"` 205 | Tid string `yaml:"typeid"` 206 | Prefix string `yaml:"prefix"` 207 | UUID string `yaml:"uuid"` 208 | } 209 | 210 | func TestValidTestdata(t *testing.T) { 211 | assert.Greater(t, len(validYML), 0) 212 | var testdata []ValidExample 213 | err := yaml.Unmarshal(validYML, &testdata) 214 | if err != nil { 215 | t.Errorf("Failed to unmarshal testdata: %s", err) 216 | } 217 | assert.Greater(t, len(testdata), 0) 218 | 219 | for _, td := range testdata { 220 | t.Run(td.Name, func(t *testing.T) { 221 | tid := typeid.Must(typeid.FromString(td.Tid)) 222 | if td.UUID != tid.UUID() { 223 | t.Errorf("Expected %s, got %s", td.UUID, tid.UUID()) 224 | } 225 | if td.Prefix != tid.Prefix() { 226 | t.Errorf("Expected %s, got %s", td.Prefix, tid.Prefix()) 227 | } 228 | }) 229 | } 230 | } 231 | 232 | func TestConstructorError(t *testing.T) { 233 | _, err := typeid.FromUUID[typeid.AnyID]("00000000-0000-0000-0000-000000000000") 234 | assert.Error(t, err) 235 | assert.True(t, errors.Is(err, typeid.ErrConstructor)) 236 | 237 | _, err = typeid.FromUUIDBytes[typeid.AnyID]([]byte{0, 0, 0, 0, 0, 0, 0, 0}) 238 | assert.Error(t, err) 239 | assert.True(t, errors.Is(err, typeid.ErrConstructor)) 240 | } 241 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package typeid 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.jetify.com/typeid/base32" 7 | ) 8 | 9 | func validatePrefix[T Subtype](prefix string) error { 10 | if len(prefix) > 63 { 11 | return fmt.Errorf("invalid prefix: %s. Prefix length is %d, expected <= 63", prefix, len(prefix)) 12 | } 13 | 14 | if len(prefix) > 0 && prefix[0] == '_' { 15 | return fmt.Errorf("invalid prefix: %s. Prefix should not start with an underscore", prefix) 16 | } 17 | 18 | if len(prefix) > 0 && prefix[len(prefix)-1] == '_' { 19 | return fmt.Errorf("invalid prefix: %s. Prefix should not end with an underscore", prefix) 20 | } 21 | 22 | // Ensure that the prefix only has lowercase ASCII characters 23 | for _, c := range prefix { 24 | if (c < 'a' || c > 'z') && c != '_' { 25 | return fmt.Errorf("invalid prefix: '%s'. Prefix should only contain characters in [a-z_]", prefix) 26 | } 27 | } 28 | 29 | if !isAnyID[T]() { 30 | expected := defaultType[T]() 31 | if expected != prefix { 32 | return fmt.Errorf("invalid prefix: '%s'. Subtype requires prefix to match '%s'", prefix, expected) 33 | } 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func validateSuffix(suffix string) error { 40 | if len(suffix) != 26 { 41 | return fmt.Errorf("invalid suffix: %s. Suffix length is %d, expected 26", suffix, len(suffix)) 42 | } 43 | 44 | if suffix[0] > '7' { 45 | return fmt.Errorf("invalid suffix: '%s'. Suffix must start with a 0-7 digit to avoid overflows", suffix) 46 | } 47 | // Validate the suffix by decoding it, it must be a valid base32 string 48 | if _, err := base32.Decode(suffix); err != nil { 49 | return fmt.Errorf("invalid suffix: %w", err) 50 | } 51 | return nil 52 | } 53 | --------------------------------------------------------------------------------